User guide
- Concepts
- How a request flows
- Trust model
- Policy reference
- Credentials
- Attestation
- Audit records and claims
- CLI
- HTTP API
- TRACE records
- The Cloudflare adapter
- Deploying TAP
- FAQ
Concepts
| Role | Who | Trusted for |
|---|---|---|
| Owner | The enterprise whose data, credentials and policy are at stake | Everything. Holds the owner key (P-256) on its own machine |
| Agent | The model and code in the sandbox — the owner's own, open source, or a vendor's | Nothing. Its behavior depends on what it reads, so TAP never relies on it |
| Platform | The sandbox provider (e.g. Cloudflare) | Running the sandbox. Not for enforcing the policy, keeping credentials or keeping the audit |
| TAP | The proctor, in a confidential VM | Exactly what its attested code does — which the owner checks |
TAP does not try to understand the agent. It decides on what leaves the sandbox: each HTTP(S) request, by host, method, path and size.
How a request flows
- The platform opens a session with TAP for each sandbox, authenticated with the platform token whose SHA-256 the owner put in the policy.
- Each request the sandbox sends goes to the platform's adapter, which asks TAP (
decide):- decide mode hosts: TAP allows or denies; the adapter sends allowed requests itself and reports the response's status and hash (
complete). - relay mode hosts: TAP answers relay; the adapter hands TAP the whole request (
relay); TAP adds the credential, sends it, records it and returns the response. - anything else: denied, with the reason; the agent sees an HTTP 403
Blocked by TAP: <reason>.
- decide mode hosts: TAP allows or denies; the adapter sends allowed requests itself and reports the response's status and hash (
- When the sandbox ends, the platform ends the session; TAP signs a claim over the session's records.
- The owner fetches the records and the claim, and checks them against TAP's attested key.
If the adapter cannot reach TAP, it sends nothing (HTTP 503).
Trust model
What TAP guarantees
- The policy is yours. TAP takes a policy only if your key signed it, and only with a higher version. Each session runs under the policy current when it opened; if the policy changes, the session's later requests are refused.
- Credentials stay in TAP. They are sealed on your machine to TAP's attested X25519 key and only ever used inside TAP, in relay mode. The sandbox and the platform never see them; a header the sandbox sets with the same name is replaced.
- The audit cannot be edited. Every request TAP saw — allowed or denied — is in a hash chain; TAP signs the chain's head with its attested Ed25519 key. Removing, changing or reordering a record breaks the check.
- Attestation is checked by you. The SEV-SNP report is verified up to AMD's root certificates (pinned by fingerprint), and it binds TAP's keys and current state (owner, policy).
Deployment shapes
| Separate (today) | Co-located (later) | |
|---|---|---|
| Where | Sandbox at the platform; TAP in its own confidential VM | Sandbox and TAP in one confidential boundary |
| Policy, credentials, audit protected | yes | yes |
| All traffic goes through TAP | relies on the platform routing it (credentialed requests cannot bypass TAP: the credential is only there) | yes |
| The agent's data hidden from the platform | no — the sandbox is not a TEE | yes |
Current limitations (developer preview)
- TAP's code is not yet in the hardware measurement on GCP. The launch measurement covers the VM's firmware; TAP is installed afterwards. Until TAP ships as a measured, bootable image, the attestation proves the hardware and TAP's keys, not which TAP build holds them. This matters most when the platform, not you, operates TAP.
- State is in memory. After a restart: new keys, no owner, no policy, no credentials — provision again.
- First come, first owner. The first key registered becomes the owner; the attestation statement shows the owner's key hash, so a client notices someone else got there first. Tenant-bound registration is planned.
- One tenant per TAP instance.
- No rollback protection for state (moot while state is in memory).
- HTTP(S) egress only. Command execution and file access inside the sandbox are not governed yet.
Policy reference
A policy is JSON, signed by the owner over SHA-256("tap-policy/v1" | policy text).
{
"v": 1,
"version": 2,
"caller_token_sha256": "ff63db0a…",
"egress": [
{ "host": "api.github.com", "methods": ["GET"], "paths": ["/repos/acme/demo/"], "mode": "relay",
"credential": "github", "inject": { "header": "Authorization", "format": "Bearer {}" } },
{ "host": "api.github.com", "methods": ["POST"], "paths": ["/repos/acme/demo/issues/"], "mode": "relay",
"credential": "github", "inject": { "header": "Authorization", "format": "Bearer {}" }, "max_body": 16384 },
{ "host": "pypi.org", "methods": ["GET"], "mode": "decide" }
]
}
| Field | Meaning |
|---|---|
v |
Always 1 |
version |
Positive integer; each new policy must be higher (the CLI and console bump it) |
caller_token_sha256 |
SHA-256 (hex) of the platform token: who may open sessions |
data_class |
Optional label ([a-z0-9-], e.g. confidential): the highest data class you declare for these sessions, carried into TRACE records |
egress |
The rules. Anything no rule allows is denied |
A rule:
| Field | Required | Meaning |
|---|---|---|
host |
yes | Lowercase host name, or *.example.com for its subdomains (not example.com itself) |
methods |
yes | Some of GET HEAD POST PUT PATCH DELETE |
paths |
no | Path prefixes starting with /. /repos/acme/demo/ matches /repos/acme/demo and everything under it, not /repos/acme/demo2. All paths if absent |
mode |
yes | decide (the platform sends it) or relay (TAP sends it) |
credential |
no | Name of a sealed credential; needs relay and inject |
inject |
with credential |
{"header": NAME, "format": TEXT}; {} in format is replaced by the secret |
max_body |
no | Largest request body in bytes (default 1 MiB) |
Unknown fields make a policy invalid, so a typo cannot loosen it.
Request matching. Only https:// on the default port, with no user part. Paths are normalized first (/a/b/../c is /a/c); encoded slashes (%2f, %5c) are refused. The query string does not take part. The first rule matching host, method and path applies.
Credentials
- Attest TAP (the statement names its X25519 key).
- Seal: ephemeral X25519 with TAP's key → HKDF-SHA256 (salt: both public keys; info
tap-credential/v1) → AES-256-GCM, with the credential's name as associated data. - Sign the name and the sealed envelope with the owner key and upload.
The console and sdk/cli.ts credential do all three. A credential sealed to another TAP, for another name, or not signed by the owner is refused.
Attestation
POST /v1/attest {"nonce"} returns:
statement:{"v":1,"kind":"tap-attest","nonce","platform","sign_key","kx_key","owner","policy","version","booted"}sig: the statement signed withsign_key(Ed25519)evidence: the SEV-SNP report (base64), whose 64-byte report data isSHA-512(statement)
The client checks: the nonce is its own; sig verifies with sign_key; the report's report data is the statement's SHA-512; the report is signed by the chip's VCEK, which AMD's ASK and ARK certify (fetched from AMD's Key Distribution Service, ARK pinned); the guest policy does not allow debugging; optionally, the launch measurement is one you accept (--measurement).
TAP's keys are new at every start, so attest again after a restart.
Audit records and claims
A record (one per request):
{"seq":5,"method":"GET","host":"paste.example-logs.dev","path":"/ci-run-4417","mode":"decide",
"decision":"deny","reason":"the policy does not allow this host","req_sha256":"e3b0…","status":null,"resp_sha256":null}
Bodies are never recorded, only their SHA-256.
The chain: h0 = SHA-256("tap/1" | session id), hn = SHA-256(hn-1 | record n) with records as compact JSON in the field order above.
The claim, signed by TAP at the end:
{"v":1,"kind":"tap-session","session","sandbox","platform":"amd-sev-snp","owner","policy","version",
"started","ended","calls","chain"}
sdk/cli.ts audit and the console recompute the chain from the records and check the claim against the attested sign_key, the owner and the policy.
CLI
node sdk/cli.ts keygen OWNER.pem
node sdk/cli.ts token
node sdk/cli.ts provision --url URL --owner-key PEM [--policy FILE] [--credentials ENVFILE]
node sdk/cli.ts credential --url URL --owner-key PEM --name NAME --from-env VAR
node sdk/cli.ts audit --url URL --owner-key PEM [--session ID]
node sdk/cli.ts trace --url URL --owner-key PEM --session ID [--out FILE]
common: --measurement HEX (repeatable) --allow-software (development only)
trace exports an ended session as a TRACE Trust Record and checks it. provision attests, registers your key if TAP has no owner (and stops if TAP belongs to another key), and installs the policy at a version above TAP's. credential reads the secret from an environment variable, so it never appears on the command line. provision --credentials seals every TAP_CREDENTIAL_<NAME>=<secret> in a local env file (name lowercased, _ to -), for a TAP that restarted.
HTTP API
| Who | Call | Body → reply |
|---|---|---|
| anyone | GET /v1/health |
→ {ok, platform, booted} |
| anyone | POST /v1/attest |
{nonce} → {statement, sig, evidence} |
| owner | POST /v1/owner |
{key} (P-256, 04‖X‖Y, base64) → {ok}; 409 if there is one |
| owner | POST /v1/policy |
{policy, sig} → {version} |
| owner | POST /v1/credentials |
{name, sealed, sig} → {ok} |
| owner | GET /v1/sessions |
header x-tap-owner: <ms>.<sig> → {sessions} |
| owner | GET /v1/sessions/ID |
same header → {records, claim} |
| owner | GET /v1/sessions/ID/trace[?nonce=N] |
same header (signed over the path without the query) → a TRACE Trust Record (profile) |
| platform | POST /v1/sessions |
{sandbox, model?} → {session}; model = {provider, model_id, version?}, the agent's model as the platform states it |
| platform | POST /v1/sessions/ID/decide |
{method, url, body_sha256, body_size} → {allow, seq} or {allow:false, reason, seq} or {allow:false, relay:true} |
| platform | POST /v1/sessions/ID/complete |
{seq, status, resp_sha256} → {ok} |
| platform | POST /v1/sessions/ID/relay |
{method, url, headers, body} → {allow:true, status, headers, body} or {allow:false, reason} |
| platform | POST /v1/sessions/ID/end |
→ {claim, sig} |
Platform calls carry Authorization: Bearer <platform token>. Owner reads are signed over GET\n<path>\n<ms> with label tap-read/v1, valid for five minutes.
The Cloudflare adapter
cloudflare/ is a Worker. Its AgentSandbox Durable Object runs one task per sandbox:
- opens a TAP session;
- routes the container's outbound traffic to the
TapGatewayentrypoint —interceptOutboundHttps("*")andinterceptAllOutboundHttp— before the container starts, and starts it withenableInternet: false; - trusts the sandbox's interception CA, writes the demo agent into the container and runs it;
- streams the agent's events back, ends the session and returns TAP's claim.
TapGateway is src/gateway.ts: decide, relay, refuse or fail closed, as in How a request flows. It has no Cloudflare-specific code, so it can be reused by other adapters.
The demo agent (cloudflare/sandbox/agent.mjs) is a model with one bash tool, using an OpenAI-compatible API. It sends the placeholder Bearer via-tap as its model key; TAP replaces it.
Deploying TAP
TAP needs Node.js 22.18+ and, for hardware evidence, Linux 6.7+ with configfs-tsm (/sys/kernel/config/tsm/report) in an AMD SEV-SNP guest.
node src/server.ts --port 443 --backend tsm --tls-cert FILE --tls-key FILE
node src/server.ts --port 8443 --backend software # development only: no hardware evidence
deploy/gcp/ creates a GCP n2d SEV-SNP VM and installs TAP behind a Let's Encrypt certificate. TAP terminates TLS itself, inside the confidential VM.
FAQ
Does TAP speak a standard audit format? Each ended session can be exported as a TRACE v0.2 Trust Record, the format cMCP and other agent-governance tools use.
Does the agent need changes? No. It makes ordinary HTTP requests; the platform routes them through TAP.
Does TAP read request bodies? In relay mode it forwards them; in either mode it records only their hashes.
What if the platform skips TAP? In the separate shape, requests the platform sends without asking TAP are not in the audit — but they cannot carry your credentials, which only TAP holds. The co-located shape removes this gap.
Why open source? TAP asks you not to trust its operator. You should be able to read the code and, with measured images, check that the attested build is this code.