Skip to content

About

Identity and credential gateway for AI agents. Ed25519 agent passports, short-lived work-visas, per-agent budgets and scopes, an instant kill switch, and publicly verifiable signed receipts — your provider key stays in the vault and the agent never holds it.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

81 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PassControl

An identity and credential gateway for AI agents. The agent never holds the real provider key: PassControl authenticates it, checks scope, policy and budget, then injects the key and forwards the request. Call logs and signed receipts let you inspect what happened, with explicit limits on that evidence.

Use it when you want to give each agent its own permissions, spending allowance and stop control without copying provider secrets into every tool. Bring your own provider key.

Start here Choose it when You operate
Cloud — free beta, open sign-up You want a managed gateway without running a database or Docker Your agents and provider account
Self-host — full working core You want the gateway and provider credentials on infrastructure you control Gateway, dashboard, Supabase Auth + Vault + database, and Redis

A Passport is an Ed25519 signing key used to obtain a short-lived visa. A Direct Agent Key is a lower-assurance bearer credential bound to one agent; its calls are not passport-signed. Both go through gateway controls.

Built by one developer under Vertias. Early software, not independently audited. Start with a non-critical provider key. Source-available under BSL 1.1, except the Claude Code plugin in plugins/passcontrol/, which is MIT.

Cloud

Create a free account. To look around first, try the keyless browser demo: it synthesizes output and does not call a billed provider.

Once you have an account, install the CLI with Node/npm and sign in:

npm install -g passcontrol
passcontrol login

Approve only the device code from your own terminal: this grants the machine a workspace control key. Login creates a Passport locally, registers its public key, and attempts a keyless governed call and receipt verification. That check needs demo and signing enabled; it is not a real-provider test.

For a real call, add a provider key in the dashboard, give the agent a matching provider/model scope and budgets, and follow the first-call tutorial. For a static-key client, use Connect an agent and copy its generated gateway URL, Direct Agent Key and concrete model into your client. The provider key stays in Vault.

Self-host

Already stuck? See self-host troubleshooting and operations.

Use Node 22+, npm, Git, Docker and the Supabase CLI. The npm package contains the CLI and compiled SDK; setup obtains the public application source and starts the local stack.

npm install -g passcontrol
passcontrol setup

Setup starts Supabase (Postgres + Auth + Vault) and Redis with a REST adapter, applies migrations, seeds a local account and opens the dashboard. There are no shared default credentials. This is a local development setup, not a production deployment.

Updating. passcontrol update updates the CLI and the checkout setup made. It shows the plan first (--check stops there), asks once (--yes for scripts), then updates an npm global install of the CLI to the exact version it found and lets the new CLI finish. For the app it fast-forwards to the public repo's main, runs npm ci, applies new migrations (listed in the plan; they only go forward), and restarts the dashboard if it was running. It leaves a checkout alone, and says why, if it has local changes, has diverged, is not a clone of the public repo, or was moved since setup (re-running the stack there would start an empty database). npx, pnpm, yarn, bun and volta installs get the right command printed instead. If an update stops part way, passcontrol update --app-only finishes it.

  1. Make a Passport call. Add a non-critical provider key in Provider Keys. Issue a Passport with a matching provider/model scope and budgets. Choose the dashboard's Sidecar or MCP configuration flow, copy its generated Passport import command into your terminal, then paste the one-time secret at the hidden prompt. The command itself contains no secret. Follow the generated client configuration and send a small prompt. Check the call and budget charge in the dashboard.
  2. Try a Direct Agent Key. Use Connect an agent, select a concrete model within scope, and copy the generated gateway URL and reveal-once key into your static-key client. Send another prompt and check its authentication method in the call log.
  3. Check the budget. Lower the test agent's token budget below the next request's reservation estimate and retry; expect a budget refusal. Restore the budget afterward.
  4. Check the kill switch. Arm it in the dashboard and retry; subsequent requests should be refused. Disarm it when finished. It cannot cancel a request already upstream.

The Passport import hands browser-issued configuration to the CLI without putting its secret in shell history. Keep the one-time secret until import succeeds. The tutorial explains the manual CLI path; client options cover SDK, Sidecar and MCP.

Limits and security status

PassControl is not independently audited. The public developer API has not shipped; the source includes a control-plane API, but that is not a launched public developer service. No independent audit or production assurance is implied by the test suite.

PassControl governs requests routed through it. It cannot prevent an agent from using another credential or network path, secure a compromised signer, guarantee a provider's usage report, or make an issuer's assertions independently true. Budget admission reserves an estimate, not a guaranteed invoice ceiling. Logging and receipt signing are best-effort; a valid receipt does not prove every call was recorded.

The gateway operator remains trusted: Cloud uses managed server-side Supabase Vault; self-hosting uses your Vault. The gateway decrypts and injects the provider key. This is credential isolation, not end-to-end encryption from agent to provider.

Short visas, stop controls, pricing tables and evidence each have further limits below. Report vulnerabilities through GitHub private vulnerability reporting. See SECURITY.md and LICENSE for security guidance and usage terms.

Connect a real agent

Use the dashboard's generated provider-native configuration: gateway base URL, Direct Agent Key and model. Anthropic uses its native base URL and key variables. Never paste a Passport private key into an API-key field.

To see the whole integration run once, examples/direct-key-worker.mjs is an ordinary OpenAI-SDK worker configured only by the agent's Setup file (passcontrol.env). It makes one governed call and prints its receipt ID, then asks for a model outside the agent's access and shows PassControl refusing it before the provider.

Add a provider credential in the dashboard and give the agent a concrete model within its scope and suitable budgets. Choose by what your client supports:

Client Integration
Static base URL + API key Dashboard Connect an agent: copy its provider-native URL and DAK
Your JS/TS code PassControl from passcontrol/sdk; SDK guide
Static-key client needing Passport passcontrol sidecar, then passcontrol env <integration>
MCP client passcontrol mcp; configure as below
Coding agent Claude Code, Codex, OpenRouter, Ollama

Direct Agent Keys point the client at the PassControl gateway origin. Passport Sidecar clients point at loopback with a provider path (for example http://127.0.0.1:8788/api/v1/anthropic) and the sidecar forwards to the gateway. The sidecar adds per-request sender proof; the current SDK and MCP paths use bearer visas and are refused when sender proof is required.

For a Passport bridge to Cloud or self-host, start this in one terminal:

passcontrol sidecar

In another terminal, print the client settings:

passcontrol env hermes

For MCP, write client configuration using the global Passport profile, with no secret in the generated configuration:

passcontrol configure claude-desktop --write

The cursor preset also writes configuration. passcontrol configure claude-code --write routes Claude Code's own model calls through the sidecar by merging two keys into its settings. passcontrol configure codex --write does the same for Codex with a profile file of its own, used as codex --profile passcontrol.

The sidecar listens on loopback by default and substitutes a visa for the client's dummy API key. Treat access to that local listener as access to the configured agent. It is not a sandbox against other processes running as you. Its provider CONNECT requests are refused; use base URLs rather than TLS interception.

To see each call as it happens, run passcontrol watch beside the agent (--port for a sidecar that is not on 8788). It is a read-only, full-screen board: one row per call the sidecar relays, with its time, declared session and sub-agent, model, BOARDED or DENIED (with the gateway's refusal code), and latency, under the agent's budget. It reads a feed on the sidecar's own loopback port, held in memory and never written to disk. A call that does not go through that sidecar is not on it, and the board cannot stop anything.

Presets (from cli/presets.mjs): generic, openhands, litellm, aider, hermes, cline, continue, chatbox, jan, msty, cherry-studio, open-webui, librechat; service presets: github, telegram, brave, notion, discord; coding agent: codex; MCP presets: claude-desktop, cursor, claude-code. Compatibility still depends on the client using supported paths. Hermes configuration.

CLI configuration and key storage

Command browser, key custody and Passport import

Run passcontrol with no arguments in an interactive terminal to open the command browser: searchable grouped commands, recent actions, and configuration-aware options. Use passcontrol --help or explicit commands in scripts.

The direct CLI call uses off/observe sender-proof mode; real provider calls need a stored key and matching scope. Key migration moves an existing file key to the OS store.

passcontrol status
passcontrol call "Say hello"
passcontrol doctor --deep
passcontrol key status
passcontrol key migrate
passcontrol logout

The CLI supports macOS Keychain, Linux Secret Service (secret-tool), and Windows DPAPI-backed storage. File configuration may contain a storage marker instead of the Passport secret; explicit environment secrets take precedence. An unavailable OS store can use an existing file fallback with a warning. Custody shown by the gateway is DECLARED evidence, not proof of storage. The signer still reads key material into process memory; this is not a non-exportable hardware key.

For a Passport issued in the dashboard for Sidecar or MCP, import it without putting the one-time secret in shell history:

passcontrol passport import --global --gateway https://YOUR-PASSCONTROL-HOST --id PUBLIC_PASSPORT_ID

The CLI proves the 32-byte Ed25519 secret matches the public ID, stores it in macOS Keychain, Linux Secret Service, or Windows DPAPI, verifies readback, and only then atomically updates the global profile. Replacement requires --replace. The public command contains no secret.

Security and protocol reference

Authentication, enforcement, budget uncertainty, providers and evidence

Request and authentication flow

Direct Agent Key ───────────────────────────────────────────────┐
                                                              v
Passport private key ── signs challenge ──> short-lived visa ──> gateway
  stays client-side                           + request proof     |
                                              when required      |
                 authentication → kill/suspend → scope/policy → budget hold
                                                                 |
                                              Vault key → provider → response
                                                                 |
                                              settlement + best-effort log/receipt

A Passport private key never reaches Cloud or the gateway. It signs challenge bytes locally to mint a work-visa; the sidecar also signs request proofs. Visas are HS256 tokens, normally valid for five minutes (VISA_TTL_SECONDS: 300–900 seconds).

Credential What the client does Boundary
Direct Agent Key (DAK) Sends a named, reveal-once pc_agent_… bearer key Bound to one agent; hash stored server-side, optional expiry, independently revocable. Works with static-key clients. Does not mint visas or authorize control-plane operations.
Passport Holds an Ed25519 keypair and signs challenges for visas Proves possession at mint. Private key stays in the SDK runtime, CLI, sidecar, or MCP process.
Passport with required sender proof Sends a fresh signed proof with each visa-authenticated request A stolen visa alone is insufficient. The proof binds method, gateway origin/path, time, nonce, and visa hash.
Control key Sends pc_… to /api/control/v1 Manages the workspace; separate from either agent credential. Keep it out of agent integrations.

Passport sender-proof modes are off, observe, and required. Off ignores proofs; observe records what would pass without enforcing it; required rejects missing, invalid, stale, or replayed proofs. Only enforced success is recorded as passport_proof_per_request; observing a valid proof does not upgrade authentication. The sidecar supplies proofs; the direct CLI call, MCP chat, and TypeScript SDK do not attach them and cannot use required mode without an additional proof implementation. Proofs do not bind the request body or query string and do not attest hardware or key storage. See authentication and lifecycle.

Passport expiry and rotation grace are checked at mint, not on every existing visa. Use suspend/revoke or a kill switch to stop subsequent requests using issued visas; these controls do not cancel an upstream request already in flight. Public lifecycle checks and the signed revocation list are separate from signature verification.

What the gateway enforces

  • An allowlist of provider endpoints, plus the agent's provider/model scope.
  • Live policy rules: model/endpoint denials, time windows, request limits; optional shadow evaluation records candidate decisions without enforcing them.
  • Token and cost admission limits through atomic reservations and settlement.
  • Platform/tenant kill switches and per-agent suspension/revocation.
  • "Ask me first" on service calls: a rule can hold the exact request until the owner approves it once, from the dashboard or with Telegram buttons.
  • Provider failover within supported request families and each attempt's authorization and budget checks; it is not arbitrary API translation. Embeddings never fail over.
  • The injected provider key is removed from what comes back: if an upstream echoes it in an error body, a JSON answer or an event stream (even split across chunks), the agent receives [REDACTED_PROVIDER_KEY] instead. This covers the exact key, not a key an upstream has transformed.

Passport scopes are snapshots in visas; scope edits can take up to the configured visa TTL to replace those snapshots. Other live controls have their own cache/invalidation behavior. Kill-state reads default to fail-open; KILL_SWITCH_FAIL_CLOSED=true blocks on kill/suspend read failure. Required sender-proof replay checks and DAK credential validation fail closed. Redis persistence and no-eviction configuration matter.

Budgets under uncertainty

Admission reserves an estimate, not a provider invoice or a guaranteed upper bound. Concurrent attempts consume reserved headroom. Complete usage settles measured tokens and table-priced cost; actual usage can exceed the estimate and cap. Anthropic cache reads/writes count toward token usage as well as their applicable price.

A broken stream, missing usage, or ambiguous dispatch is not assumed free. An uncertain settlement retains at least the estimate (or higher observed usage); an attempt whose ending never runs leaves an open hold that does not expire. may_have_dispatched means dispatch permission was claimed, not that billing is proven. Such a hold cannot be released as not_spent through recovery.

An agent can also have a daily or monthly spend limit (calendar UTC), checked in the same reservation as its cumulative caps and refused with 402 blocked_budget_period and a retry-after to the next reset. A call counts toward the period it finishes in; spend earlier in the period counts when the limit is set; an attempt that never settles keeps counting until an operator resolves it. An output ceiling (max_output_tokens in the agent's policy) refuses any request whose stated output limit is missing or larger — it is never rewritten to fit.

Lost or mismatched established budget generations cause 503 blocked_budget_state, not a fresh allowance. Recovery is an explicit operator action over retained logs and adjustments; it cannot reconstruct missing evidence. See Budget recovery.

Prices are an in-code estimate table, one row per model, read from each provider's pricing page. Under a dollar limit, a model with no row is refused 402 unpriced_model; without one it is estimated at the provider's highest listed rate. Custom endpoints are unpriced, even when the model name matches: logs mark cost unknown, receipts carry unp, and token accounting continues. They add nothing to an agent's cost budget, and an agent with a dollar limit is refused them (402 unpriced_endpoint): limit those agents by tokens. Known/table-priced cost, conservative enforced spend, and open holds are different figures. The dashboard therefore labels the all-time counter Settled budget charges and shows its durable call-attributed charges, separate operator adjustments, any visible difference, and live open reservations marked not yet charged. The call table keeps observed cost and budget charge in separate columns and can load older durable rows on demand. An unavailable database or Redis explanation is shown as unavailable, never as zero.

Supported providers and endpoints

All paths below are relative to /api/v1/<provider>. SDK base URLs and aliases are in the endpoint reference.

Provider ID Inference Discovery
openai Chat Completions, POST Responses and POST Embeddings Models list/detail
anthropic Messages Models list/detail
mistral OpenAI-compatible Chat Completions and POST Embeddings Models list/detail
groq, together OpenAI-compatible Chat Completions Models list/detail
deepseek OpenAI-compatible Chat Completions Not proxied
gemini Google's OpenAI-compatible Chat Completions Models list/detail
xai POST Responses only (legacy Chat Completions refused) Models list/detail
openrouter OpenRouter's Chat Completions, one key for its whole catalog; charged at the cost OpenRouter reports Models list
azure Azure OpenAI v1: Chat Completions, POST Responses and POST Embeddings, at the resource address stored with the key Models list
local A model server you run (Ollama, LM Studio, vLLM): Chat Completions at the address stored with the credential, self-host only Models list

Gemini's native generateContent API is not supported. Responses is served for OpenAI, xAI and Azure; embeddings for OpenAI, Mistral and Azure, and an embeddings call never fails over, because another model's vectors would not match the ones already stored. Response retrieval/deletion, files, fine-tuning, batches, and token-counting endpoints are not proxied. OpenAI's hosted tools (web search, file search, code interpreter and others billed outside tokens) and its search models are refused, because no token budget can hold their fees. Model availability still depends on your provider account.

Custom provider base URLs support compatible deployments such as Ollama, vLLM, or LiteLLM without adding provider IDs. They require operator opt-in: PROVIDER_ENDPOINT_MODE=selfhost permits HTTP, private addresses, and custom ports; a comma-separated hostname list permits only listed HTTPS hosts on port 443. Unset/off refuses custom endpoints. The local stack (npm run dev:docker, passcontrol start) runs with selfhost unless you set off: it is one developer's own gateway, so the person who sets an endpoint is the person who owns it. A hosted deployment's default stays off. Shape validation is not full SSRF prevention: it does not resolve or pin DNS. Operators must control egress and trust the destination receiving the provider credential. Upstream redirects are refused, never followed.

Receipts, statements, and identity evidence

A signed call receipt records the issuer's decision, authentication method, reported usage/cost, and (when read) a digest of client request bytes. It does not include the provider response body. Verify it with an explicitly trusted issuer:

passcontrol verify receipt "<jws>" --issuer https://your-gateway.example

The public /.well-known/jwks.json publishes instance verification keys; the hosted receipt verifier and /verify/receipt on your deployment check in the visitor's browser. A valid signature proves that key signed those bytes. It does not prove the issuer is honest, the provider billed that amount, or all calls were recorded. Logging/signing is best-effort; absent receipts prove nothing. Retain old public keys for historical verification when rotating the instance signer.

Cloud signed spend statements commit to a fixed set of receipts using a Merkle root and link to a preceding statement. Inclusion proofs establish membership in that signed set. They do not independently audit totals, completeness, or provider invoices. A chain check needs the relevant earlier statements or a trusted checkpoint. The public self-host tree ships the format and verification code/page, not statement production, scheduling, or statement/proof-serving routes. See statement format and limits.

Recorded kill-switch demonstration: subsequent requests are refused

The recording illustrates a stop control on successive calls; it does not show cancellation of an already-dispatched stream.

Self-host operations reference

Source setup, lifecycle, credential switching and portability

The CLI package declares Node ≥18, but the full stack needs Node 22+: locked Supabase dependencies need ≥20 and Wrangler needs ≥22. Use passcontrol setup --no-open to suppress browser launch. From a source checkout, the equivalent development path is:

git clone https://github.com/Vertias3u/PassControl.git
cd PassControl
npm ci
npm run dev:stack
npm run dev:docker

Local setup starts Supabase (Postgres + Auth + Vault) and Redis with a REST adapter, applies migrations, and seeds a local account. It writes .env.docker; use npm run dev:docker to load it. This is development setup, not a production deployment. There are no shared default credentials. passcontrol setup --app-dir <path> selects a checkout; passcontrol unlink forgets a remembered one.

Local lifecycle commands manage http://localhost:3000 independently from the active Cloud API gateway. setup, start, and restart verify PassControl at /api/version and require the launcher to remain alive before switching the global active gateway. status reports the active API gateway and CLI-managed local dashboard separately; open opens the active gateway. Explicit loopback ports remain supported, but a portless local URL is rejected instead of becoming port 80. --port-offset moves Supabase and Redis ports only; the dashboard remains on port 3000.

Switching from a credentialed remote gateway clears gateway-bound local credentials only—it does not revoke the remote key or Passport. Interactive use requires a separate confirmation; noninteractive use requires --forget-active-credentials, and --yes is not a substitute. Shell or project-file gateway overrides are refused and named before services start, because a global write could not override them safely. Clean up any still-active remote identity at its original gateway.

Self-host is built for one developer on their own machine; this repository does not ship a production deployment path. To let agents running elsewhere reach PassControl, or to give passports a verify page anyone can open, use Cloud.

Boundary Cloud Public self-host tree
Gateway, dashboard, auth, budgets, receipts, SDK Managed deployment Included; you operate them
Provider credentials and request processing Managed infrastructure Your infrastructure and chosen upstreams
Passport private key Client-side Client-side
Signed spend statement issuance/proof service Hosted capability Verification and format only
Invite workflow, hosted quotas and hosted-site operations Cloud-specific Not the hosted service

Workspace portability: passcontrol export --out workspace.json saves configuration; passcontrol import workspace.json previews it before an explicit import. The CLI path does not migrate provider secrets or manufacture verified owner identity. Treat exports as sensitive configuration and inspect the result for partial/refused items.

Release updates and the learning center are hosted-site resources.

For the full reference, see API documentation. Contributions: CONTRIBUTING.md.

About

Identity and credential gateway for AI agents. Ed25519 agent passports, short-lived work-visas, per-agent budgets and scopes, an instant kill switch, and publicly verifiable signed receipts — your provider key stays in the vault and the agent never holds it.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages