Install

One package, one dependency, no runtime to run.

Node and Python are ports of each other: same endpoints, same approval semantics, same timings. Pick whichever your agent already lives in. Both read the same ~/.agentvalet/agent.key, so the tooling is interchangeable and you do not need Node installed to use Python.

terminal
# Node 18+, one dependency (jose, for RS256 signing)
npm install @agentvalet/client

# Python 3.9+, two dependencies (httpx, pyjwt[crypto])
pip install agentvalet
1
Sign in at app.agentvalet.ai (it's free) and create an agent.
2
Run npx @agentvalet/register, or agentvalet register --code <your-code> from Python. The RSA keypair is generated on your machine; only the public half is sent.
3
Approve platforms and scopes in the dashboard. What you grant is exactly what this agent can reach, and nothing else.
4
Call fromEnv() / from_env() and start making calls. There is no server for you to deploy.
Already inside Claude Code, Cursor or Claude Desktop? You don't need this package. Install @agentvalet/mcp-server instead and call use_platform: same guarantees, no code at all. This SDK is for the agents that live outside an MCP host.

Full reference for both clients, including every constructor option, the environment variables each one reads, and the approval-resume semantics: Node & TypeScript and Python.

Your first governed call

The credential is never in your process.

This is the whole surface. You name a platform, an endpoint and the scope you are exercising. AgentValet signs a 60-second identity assertion for your agent, checks the call against the owner's grants and policy, decrypts the real credential in memory, makes the call, and writes an audit row. The credential never appears in a response, a log line, or the model's context.

notify.ts
import { AgentValet } from "@agentvalet/client";

// Needs no arguments on a machine that has run the register command.
const av = AgentValet.fromEnv();

const result = await av.call({
  platform: "slack",
  endpoint: "/api/chat.postMessage",
  method:   "POST",
  scope:    "chat:write",
  data:     { channel: "#general", text: "Deploy finished." },
});
notify.py
from agentvalet import AgentValet

with AgentValet.from_env() as av:

    # Deny-by-default: whatever isn't in here will raise.
    grants = av.list_platforms()

    result = av.call(
        platform="slack",
        endpoint="/api/chat.postMessage",
        method="POST",
        scope="chat:write",
        data={"channel": "#general", "text": "Deploy finished."},
        # Shown to whoever approves, if this scope is approval-gated.
        reason="Notify the team that the deploy completed",
    )
Where does it find my identity?

fromEnv() reads AGENTVALET_AGENT_ID / AGENTVALET_OWNER_ID, or the bare AGENT_ID / OWNER_ID that the register command writes. It then looks for the private key in AGENT_PRIVATE_KEY_B64, AGENT_PRIVATE_KEY_PATH, AGENT_PRIVATE_KEY, and finally ~/.agentvalet/agent.key (written mode 0600). In a container, mount the key or pass it base64-encoded; nothing else needs to travel with your agent.

Approvals

A human in the loop is just a slower function call.

Mark a scope as requiring approval and nothing about your code changes. The proxy holds the action, your call() waits, you approve from your phone, the proxy runs the call and hands you the result. From your program's point of view it simply took longer. This is the piece that makes it safe to give an unattended job a scope you would never hand it outright.

If nobody answers in time you get ApprovalTimeoutError, and that is not a failure: the action is still queued server-side. Keep the approvalId and resume with waitForApproval(approvalId) later, from this process or a completely different one.

Denials

A refusal your code can reason about.

Governance you can't handle in code is just an outage. Every failure from the SDK is a typed error, so your agent can tell "I am not allowed to do this" apart from "Slack is down" apart from "a human said no", without string-matching an error envelope.

refund.py
from agentvalet import AccessDeniedError

try:
    av.call(platform="stripe", endpoint="/v1/refunds", method="POST",
            scope="refunds:write", data={"charge": charge_id})

except AccessDeniedError as err:
    # Not a crash. A governance decision your code can act on.
    decision = av.request_access(
        platform=err.platform,
        scope=err.scope,
        reason="Refund duplicate charges flagged by support",
    )
    # decision["status"] is "approved" | "denied" | "pending"
AccessDeniedError
No grant for this platform and scope, or your policy blocked it. Recoverable: requestAccess() asks an org admin and polls for the decision.
ApprovalDeniedError
A human looked at this specific action and said no. Terminal, and the one error your agent should never retry.
ApprovalTimeoutError
You stopped waiting; the action did not. Still queued server-side, resumable from any process with the approval id.
UpstreamError
Allowed, approved, executed, and the SaaS itself returned a non-2xx. The governance layer did its job; this one is Stripe's problem, not ours.
ConfigError
Missing or malformed identity or key. Raised before any network call, so a misconfigured agent fails at startup rather than halfway through a workflow.
NetworkError
The transport itself failed, with a hint that diagnoses DNS, TLS interception, firewall or timeout rather than leaving you with a bare socket message.
Which package

Three ways in. Pick by where your agent lives.

Inside an MCP host
@agentvalet/mcp-server. Claude Code, Claude Desktop, Cursor, Codex, Factory Droid. One config block and every session is governed, with no integration code to write.
Your own code
@agentvalet/client or agentvalet. LangChain tools, cron jobs, plain services. This page.
Building your own MCP server
@agentvalet/mcp-broker. Enforce grants and policy inside a server you ship to somebody else, so your users' credentials never reach your process either.

Take the keys out of your codebase.

One install, one register command, and the credential stops living in your environment. Free to start, no credit card.