Security

Where the key is, at every hop

This page is written for the person who has to sign off on putting a production credential behind a service. It says what the design holds by construction and what your own catalogue and policy decide. Both are checkable on your own machine, and this page says how.

The architecture

Three hops and one field

Two roles run from one binary. Control holds the database, the vault and the journal. The executor makes the outbound call and lives on a network with no route to either. A credential leaves the vault once per call, inside the process that is about to use it.

Where the credential is at every hopThree hops. The agent sends an action with no credential. The gateway classifies it and brings in a person when the class calls for one. It writes the intent row, unwraps the key inside its own process and dispatches. The upstream answers and the injected bytes are replaced before the answer reaches the agent.intentunwrapresultYour agentaction, arguments, no keyno credential on this segmentControl and executor1. classify against the recipe2. ask a person, or apply a live grant3. write the intent row and seal it4. unwrap for this call, inject, dispatchPer-tenant vaultsealed, versionedthe only segment carrying the keyThe upstreama normal authenticated requestinjected bytes replaced in body, headers and Location
The yellow field and the wire below it are the only places the credential exists. Every segment your agent can reach is on the other side of that line.

The line

What the design holds, and what your catalogue decides

The first list is structural. It is true because of how the code is shaped, and tests fail when it stops being true. The second list is yours to steer, and the console shows you the whole of it for your own tenant.

Held by construction
No response to an agent carries a credential value. No route on this platform reads one back, for an owner, for support, or over the API. The executor replaces the exact injected bytes, and their base64 form, in the body, the headers and the Location of every answer. Outgoing bytes are scanned for our own token prefixes and for the injected value, and a match refuses the call.
Decided by your catalogue
Your agent does not get a secret from us. It can get a secret from an upstream if you allow it a path that hands one out. Those paths are known and named: GitHub installation tokens, GitLab CI variables, Vercel env with decrypt, Vault kv. The recipe marks them, they take the destructive class, the default is refuse, and your console lists every one of them in your own tenant.
Held by the two database roles
The control plane connects as a role that owns nothing and cannot bypass row-level security. Isolation runs at two levels, tenant and workspace, set inside every transaction. A partitioned table gets its policy on every partition, and a test over the catalogue tables fails the build if one is missing.
Held by the network
The executors sit on one network with the control plane and nothing else: no database, no object storage, no keys. They receive one credential for one call over mutual TLS. For SSH they never receive the private key at all and ask the control plane to sign each challenge.

The exact list

The paths that hand out a credential of their own

Some endpoints answer with somebody else's secret, and a GET is enough. GitLab hands over every variable a project has. Vercel decrypts an environment on request. AWS mints session credentials. Each of these is marked in the catalogue, takes the destructive class whatever its verb, and is refused until you allow it. This table is generated from the catalogue this site ships with, so it is the list the engine classifies by.

ServicePathWhat happens
Amazon Web Services^sts(\.[a-z0-9-]+)?\.amazonaws\.com$destructive, refused by default
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])CreateAccessKey$destructive, refused by default
Amazon Web Services?Action=^CreateAccessKey$destructive, refused by default
Amazon Web ServicesAction=^CreateAccessKey$destructive, refused by default
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])GetSecretValue$destructive, refused by default
Amazon Web ServicesX-Amz-Target: (?i)(^|[._])GetParameters?$ $.WithDecryption ~ ^true$destructive, refused by default
GitHub APIPOST ^/app/installations/[^/]+/access_tokens$destructive, refused by default
GitHub APIPOST ^/(repos/[^/]+/[^/]+|orgs/[^/]+)/actions/runners/(registration|remove)-token$destructive, refused by default
GitLab API^(/api/v4)?/(projects|groups)/[^/]+/variablesdestructive, refused by default
GitLab APIPOST ^(/api/v4)?/(projects|groups)/[^/]+/(deploy_tokens|access_tokens|runners)destructive, refused by default
Sentry APIGET HEAD POST ^/api/0/projects/[^/]+/[^/]+/keys/destructive, refused by default
Slack Web API^/api/(oauth\.(v2\.)?access|apps\.connections\.open|tooling\.tokens\.rotate)$destructive, refused by default
Vercel API/env ?decrypt=^true$destructive, refused by default
Vercel APIGET HEAD ^/v[0-9]+/projects/[^/]+/env/[^/]+$destructive, refused by default

Your console shows the same list for your own tenant, filtered to the bindings your agents actually have.

The vault

Envelope encryption, and no way to read a value back

Four steps, each one narrower than the last. A root wrapper on the host opens a per-tenant key. That opens a per-domain key: one for credentials, one for the journal, one for the bodies and SSH session recordings you choose to keep. From it a per-record subkey is derived with HKDF-SHA256. The record is sealed with ChaCha20-Poly1305 under an authenticated header of tenant, domain, record and version. Change any of the four and the record stops opening.

One unwrap path, and it opens on one state
The engine unwraps a value for a call that has already moved to dispatching. There is no second caller, no export, no support tool and no admin screen, and a test walks the package to keep it that way.
The root key has a second copy from the minute it exists
At generation it is split into three shares, and any two of them rebuild it. One share with the owner's secret manager, one offline, one in a cloud key service. Taking a share is an event in the platform journal.
A daily self-test, so a monitor tells you first
A job unwraps a test key every day and publishes the result as a metric with an alert on it. The failure mode this catches is the one where everything answers and every call with a credential fails.

The journal

Written before the call, sealed away from its writer

The intent row and the chain update share one transaction with the dispatch decision, so the record exists before the side effect does. Sequence numbers come from a counter row rather than a database sequence. A sequence hands out numbers that survive a rollback, and an unexplained gap is a gap an auditor reads as a deletion.

Sealed with age, to a recipient the writer does not hold
Each event body is encrypted to a per-tenant X25519 recipient before it is hashed. The process that writes the journal holds the public half and reads nothing back.
Checkpoints you keep, signed with Ed25519
A Merkle root over the chain, signed, available over the API and pushed to your SIEM with the sequence number and the row hash. Your SIEM becomes a witness for the segment it holds. The chain format carries its own version string, spc-chain/1, so a verifier written today keeps working against an export taken tomorrow.
A verifier that holds none of our keys
Point the open-source tool at any export. It recomputes the chain, checks the signature and tells you where the first break is. A corrupted row in the golden export is part of our own test suite.
One tenant's chainFive sealed rows, each hashed over the one before it, under a signed checkpoint.#4129f2c…#41341ab…#4140d77…#415b3e1…#4165c8a…each row hashes the one before itsealed to a key the writer does not holdsigned checkpointyours to keep, and to hand to an auditor

Your data, not only your keys

The bodies your calls carry

A credential is not the only thing that moves through here. The Stripe response with a customer record on it, the diff from GitHub, the output of an SSH command: they all pass through the executor. Here is what happens to them.

Capture is off until you turn it on
A binding keeps request and response bodies only when you ask it to. Until then the journal holds the shape of the call and its result, and none of the content.
What is kept is sealed the way the journal is
Stored bodies and SSH session recordings sit under their own per-domain key, encrypted per record, referenced from the journal by handle. They follow the retention on your plan and their own time to live.
Notifications carry no content, ever
A Telegram card or an email holds the target of a call and a link to an authenticated screen. A decision is only ever taken on a surface you signed in to.
Deleting a tenant destroys the keys that open them
Crypto-shredding, rather than a delete job you have to trust. After it, no record of that tenant opens again, here or anywhere else.

The executor holds a body for exactly as long as the call takes. It has no database, no object storage and no route to either.

Egress

An agent cannot aim the gateway at the gateway

The deny list is compiled into the binary and configuration can only add to it. Private ranges, loopback, link-local, carrier-grade NAT, the deploy network and our own hostnames are refused whether or not a binding exists for them.

The name is resolved again on every execution
Including the execution that follows a wait for approval. A host that was public when a person answered the card can point at a metadata address by the time the call runs. The address is resolved and pinned at dispatch, and the call goes to the address that was checked.
Redirects with a credential on them stop
A 3xx comes back to the agent as a result. The gateway does not follow it with your key attached.
A limit sends the call to approval
Three limiters run at once, per agent, per binding and per tenant. When one fires, the call goes to approval rather than to a dead end. A rate limit answers with a retry time, in words a model can act on.

The specification

The MCP revision of 2026-07-28 asks for this shape and forbids the other one

A server that holds tokens bound to a user must not transmit them to the client. Passing a client's token through to an upstream is refused in the same words. Every vendor that hands your agent a raw token is on the other side of that sentence.

Both live revisions, one handler each
The 2026-07-28 revision is stateless: no session header, no server-sent stream, discovery instead of initialize. The 2025-11-25 revision keeps its session. One handler doing both is wrong for both.
The tool list is private to a binding
Your list differs per agent, so it is marked private. A client that caches it shared would show one tenant another tenant's tools.
An authorization server with the flags the connectors need
Protected resource metadata, client id metadata documents, dynamic registration for compatibility, proof key exchange, the resource indicator and the issuer parameter. A consent registry per user and client, with a consent screen written against the specification's own checklist for a proxy holding a static client id.

When we are down

You keep working without us

Every workspace has a printable sheet in the console, kept current by the product. Each binding, the service, the credential, the provider that issued it, and the URLs to reissue and revoke it. A team that cannot work while we are down is a team that will not put us on a critical path.

Pending approvals survive a restart
State lives in the database. A control plane deploy does not drop a waiting call, and an agent continues on the same call id.
An unknown outcome stays unknown
If a replica dies after the upstream acknowledged the request, the call is marked with an outcome nobody knows and it is never retried automatically. A duplicate charge is worse than a missing row.
A stolen agent token is a detectable, revocable, bounded incident
A new origin raises a signal, the agent pauses, the token rotates with an overlap, and the journal shows exactly which calls carried it.

Read the verifier before you trust the journal

The tool is open, the export is a file, and the check runs on your machine with none of our keys.

Connect my first agent

Free for 10,000 calls a month. Every plan exports the journal and every plan gets checkpoints.