Skip to content

agent-custody ​

Chain of custody for AI agents: a signed receipt for every tool call, checkable with public keys alone. What each check proves, and against whom, is a table below.

Two producers, one receipt format, one verifier.

  • The gateway is an MCP proxy between an agent and the systems it can affect: MCP servers, REST APIs described as tools, and other agents it delegates to over A2A. For every tool call, allowed or denied, it checks a delegation grant signed by the human principal and any required human approval of the exact arguments, gathers the facts the policy needs by calling upstream itself, evaluates a Cedar policy that fails closed, forwards the call only on allow, and emits a signed receipt appended to a Merkle transparency log.
  • The SDK is an interceptor inside the agent's own process, hooked into the framework's tool-call callbacks: Claude Code, the Claude Agent SDK, the OpenAI Agents SDK, the Vercel AI SDK, OpenClaw, DeepSeek Harness, Hermes, Google ADK, LangChain, or any function you wrap. It reaches everything the gateway cannot see and issues the same receipts, labelled as self-reported.

Anyone holding the public keys can verify a receipt offline. The agent is not trusted. The layer around it is, and the receipt says exactly how far that trust extends, starting with who issued it. What the receipt file carries of a call is a choice per tool: a field can be withheld as a commitment or encrypted under your key, and the verifier recomputes the digests when it is shown the values.

  • Reference: every function, endpoint, MCP tool, and command with its request and response
  • Tutorials: thirty-two runnable examples, one per aspect of the code, all executed by the test suite
  • Financial operations: durable operation ids, provider idempotency keys, atomic daily budget reservations, and reconciliation receipts for uncertain outcomes
  • The gateway console: an admin service for the person who runs a gateway: config, tools, receipts with a verdict each, a policy editor that replays recorded calls before it saves, grant revocation, and an audit trail
  • Configuring the gateway: who may use which tools and where the tools live, a worked example with two agents, the Docker image, and agent-custody check to validate a config before it starts
  • Usage guide: gateway setup, wiring into Claude Desktop, Claude Code, or your own agent loop, and the gateway as an A2A agent between agents that delegate to each other, private evidence: what the receipt file carries of each call, and searching receipts and exporting offline cases with the state package's review console
  • Live Codex recovery demo: a real support agent, incorrect CRM memory, operator-reviewed compensation, and verified receipts; an opt-in live run with offline regression coverage
  • Human approvals: short-lived signatures over the exact action, two-person thresholds, single-use enforcement, and offline verification
  • The interceptor SDK: Claude Code hooks, the Claude Agent SDK, adapters for the OpenAI Agents SDK, Vercel AI SDK, LangChain, OpenClaw and DeepSeek Harness, a Hermes Agent plugin and Google ADK callbacks through the Python package, and wrapping tool functions in anything else
  • Writing policies: how a tool call becomes a Cedar request, with tested examples, and replaying history against a proposed policy with policy simulate
  • Verifying a receipt: what each check means and what a verified receipt does and does not prove
  • What the evidence satisfies: the receipts, packs, and certificates mapped to SOC 2, ISO 27001, the EU AI Act, and UK GDPR, with what none of them claims
  • Threat model: every party who could make a receipt false, the move, what stops it, and whether that is a property of the evidence or of the deployment; and what is not defended

Getting started ​

bash
npm install @agent-custody/receipts        # or bun add, pnpm add

Published on npm as @agent-custody/receipts: compiled JavaScript with type declarations, Node 22 or later, Apache-2.0.

Record receipts from inside your own agent, no gateway needed. Generate a signing key, point a config at it, wrap the functions the agent calls:

bash
npx agent-custody keygen --dir keys --name app
json
{ "agentId": "billing-bot", "identity": { "keyFile": "keys/app.key" }, "receiptsDir": "receipts", "logFile": "log.jsonl" }
ts
import { createSdkIssuer, loadSdkConfig, loadPublicKey, verifyBundle } from "@agent-custody/receipts";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const refund = issuer.wrap("stripe.refund", async (args: { amount: number }) => stripe.refund(args));
await refund({ amount: 5000 });                 // one signed receipt in receipts/, one leaf in log.jsonl

const bundle = JSON.parse(readFileSync("receipts/<id>.json", "utf8"));
verifyBundle(bundle, { issuerKeys: [loadPublicKey("keys/app.pub")], logFile: "log.jsonl" }).ok;   // true

Framework hooks and adapters, including Claude Code, the OpenAI Agents SDK, the Vercel AI SDK, LangChain, OpenClaw, DeepSeek Harness, Hermes and Google ADK, are in docs/sdk.md. To enforce rather than record, put the gateway between the agent and its tools: npx agent-custody gateway --config gateway.json, set up in docs/usage.md; between an agent and the agents it delegates to over A2A, --a2a serves the gateway as an A2A agent itself. The gateway is listed in the MCP Registry as io.github.svayatta/agent-custody; its entry is server.json, published after each npm release by running the mcp-registry workflow from the Actions tab (GitHub OIDC, no token).

How it fits together ​

The architecture guide adds the customer-side and hosted split, the data model, and sequence diagrams for onboarding, revocation, and verification.

Three parties hold keys. The principal signs a grant saying which agent may use which tools until when. The issuer, gateway or SDK, signs every receipt and every tree head. The verifier holds only public keys and needs no access to the issuer, the agent, or the upstream system.

Two producers, one receipt ​

gatewaySDK
where it runsseparate process between agent and toolsinside the agent's process
what it seesMCP tool callswhatever the framework's hooks expose
enforcementyes, denied calls never reach upstreamonly where a hook can block
provenance of its fieldsattested and observedclaimed, all of them
what a verifier learnsthe agent could not skip or forge thisthe agent's process reported this and it has not changed since
installone line in the host's MCP configa hook entry or a wrapped function

Every receipt names its issuer, and the verifier prints what that issuer kind is worth before anything else. A dashboard full of sdk rows is the reason to route the consequential calls through the gateway.

Supported hosts and frameworks ​

host or frameworkproducerenforce + recordrecord onlytested against
any MCP host: Claude Desktop, Claude Code, Cursor, customgatewayyesa real MCP client and upstream over stdio
Codex CLI (headless)gatewayyesa manual codex exec run and offline MCP recovery checks; guide
Claude CodeSDKhook command, PreToolUse denyPostToolUsethe documented hook contract, via stdin
Claude Agent SDKSDKclaudeAgentHookssamethe same handler
OpenAI Agents SDK (JS)SDKwrapToolsobserveRunnera real Runner with a scripted model
Vercel AI SDKSDKwrapToolsa real generateText loop over the SDK's mock model
LangChain / LangGraph (JS)SDKtool(issuer.wrap(fn))ReceiptCallbackHandlerreal StructuredTool invocations
OpenClawSDKregisterOpenClaw / openclawHooks: before_tool_call denyafter_tool_callOpenClaw's documented hook contract, in the plugin's process
DeepSeek HarnessSDKthe module is the plugin: tools/pre-execute denytools/post-executethe harness's tool waterfall types, driven as the harness drives them
Hermes Agentsidecar + Python packageagent_custody.hermes: pre_tool_call blockpost_tool_callthe documented hook contract, against a live sidecar
Google ADKsidecar + Python packageadk_callbacks: before_tool_callback skipsafter_tool_callbackthe real package, ADK's runner with a scripted model
anything elseSDKissuer.wrap(name, fn)issuer.recordplain functions
Python: LangChain, OpenAI Agents SDK, CrewAI, Claude Agent SDKsidecar + Python packagewrap_tools (OpenAI Agents, CrewAI), claude_hook PreToolUse deny, client.wrapReceiptCallbackHandlerthe real Python packages, receipts checked by this verifier
Go, Java, Rust, any language with HTTPsidecardecide then recordrecordexamples/languages, each run against a live sidecar
any MCP host in any language: Claude Agent SDK Python, OpenAI Agents Pythongatewayyesthe gateway is an MCP server; usage.md
any REST API, as tools the agent reaches through the gatewaygateway, rest upstreamyesa stand-in HTTP API; usage.md
any agent reached over A2A (Google ADK RemoteA2aAgent, or any A2A client)gateway, a2a upstream and gateway --a2ayesstand-in A2A agents in both wire formats; ADK's own to_a2a and RemoteA2aAgent in the Python suite; usage.md

The framework packages, pg, and @node-saml/node-saml (for SAML sign-in to the portal) are optional peer dependencies. Each adapter imports only from its own package.

One tool call, end to end ​

Denied calls get receipts too. "The agent tried to pay out funds and was refused" is evidence worth keeping.

Anatomy of a receipt bundle ​

Every field carries a provenance label. This is the design decision that matters most, and it is what a verifier reads back.

provenancemeaningtoday's examples
attestedsigned by a key other than the issuer'sprincipal id, agent id, the delegation grant (gateway receipts)
observedthe issuer obtained it deterministically, outside the agent's controlupstream tool results, fact lookups, the policy decision, execution status (gateway receipts)
claimedoriginated from the agent, the model, or the agent's own process, no independent checktool arguments, the model id, session ids, and every field of an SDK receipt

Quick start ​

bash
bun install                              # from the repository root, once for the workspace
cd packages/receipts
node scripts/demo.ts                     # gateway: keys, grant, policy, four tool calls, verification, a tampering attempt; then the SDK wrapping the same tool
node examples/01-keys-and-signing.ts     # first of thirty-two step-by-step examples, see docs/tutorials.md
bun run test                             # this package; `bun run test` at the root runs every package

Everything runs on plain Node 22 or later. No build step, no npx, no tsx needed on the command line.

The demo leaves everything in demo-out/, including receipts from both producers. Verify a receipt by hand:

bash
node src/cli.ts verify demo-out/receipts/<id>.json \
  --issuer-key demo-out/keys/gateway.pub \
  --principal-key demo-out/keys/principal.pub \
  --log demo-out/log.jsonl

Exit code 0 means every check passed. See docs/verification.md for what the report means.

To issue receipts from your own agent code, without a gateway:

ts
import { loadSdkConfig } from "./src/config.ts";
import { createSdkIssuer } from "./src/sdk/index.ts";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const refund = issuer.wrap("stripe.refund", async (args: { amount: number }) => stripe.refund(args));

docs/sdk.md has the config file and the per-framework adapters.

What a receipt proves, and what it does not ​

claimverifieradversarymechanismstatus
receipt was not altered after issueanyonelater editorDSSE signature over canonical JSONdone
receipt was issued in sequence, none removedauditor with a log copyoperator rewriting historyMerkle inclusion proof + recomputed rootdone
the principal really delegated this scope to this agentanyone with the principal's keyoperator, agentgrant signed by principal key, checked for scope and validity windowdone
the policy decision was made against this exact policyanyoneoperator swapping policiessha256 of policy text in the receiptdone
facts the policy relied on were not asserted by the agentanyone reading the receiptagentfacts fetched by the gateway, recorded with their own digests, labelled observeddone
the side effect was committed to the log before it happenedanyone with a log copyoperator, log outagefor tools in precommit, a signed authorization logged first, embedded in the receipt with its leaf position, checked to precede the receiptdone
the upstream system actually executed the actionanyone with the upstream's key or the provider's secretoperatorthe upstream signs its result for the receipt, or the provider's own delivery signature (Stripe, GitHub) is embedded and checkeddone, for upstreams that sign
the operator itself cannot mint a false receiptregulator, counterpartyoperatorneeds a TEE-hosted signer or a federated lognot done
which model produced the callanyoneoperatorno hosted provider signs model identitynot possible today, labelled claimed
an SDK receipt reflects what the tool really didanyoneagent's own processnone; the SDK shares a process with the agentby design not claimed; issuer kind sdk says so

If a vendor tells you their receipts prove more than the first five rows, ask them which key signed it.

Layout ​

src/config.ts      gateway and SDK config schemas, path resolution
src/crypto.ts      canonical JSON, sha256, Ed25519 keys, DSSE sign/verify
src/log.ts         Merkle log: append, root, inclusion and consistency proofs, verify, JSONL persistence
src/log-check.ts   the outside monitor: verifies the head, checkpoints, and witness of a running log
src/portal.ts      the tenant portal: register, first key, usage against plan, keys, Stripe billing, export, member roles, TOTP MFA, single sign-on, on the log's Postgres
src/sso.ts         enterprise sign-in for the portal: OIDC (code and PKCE, id_token checked against the IdP's key set) and SAML 2.0 (signed assertions only, through @node-saml/node-saml)
src/log-export.ts  a tenant's export of their own log, self-checked, as a log file the verifier reads
src/witness.ts     the witness: countersigns the log's checkpoints from another operator's machine, or refuses with an alarm
src/signer.ts      the signer: the log's key in its own process, the key document verifiers fetch
src/checkpoints.ts signed heads published on a schedule, to files and to Postgres
src/log-store.ts   the log server's backends: the file, and Postgres with tenants, hashed tokens, one writer per tenant, rate limits
src/log-sink.ts    where leaves go: the local file, or a remote log over HTTP; plus the reference log server
src/policy.ts      Cedar evaluation wrapper, fail-closed
src/simulate.ts    replay recorded policy decisions against a proposed Cedar policy, grouped changes and run digest
src/delegation.ts  signed delegation grants
src/approval.ts    exact-action human approvals, distinct trusted signatures, persistent single-use consumption
src/receipt.ts     receipt and authorization statement types and provenance labels
src/issue.ts       sign, log, and write a receipt, or commit an authorization first; shared by both producers
src/gateway.ts     the MCP proxy: a host (key, policy, upstreams, log) and a session per grant; scope check, facts, policy, forward, receipt
src/operations.ts  SQLite operation claims and daily budget reservations, retained across retries and process crashes
src/gateway-http.ts the gateway over Streamable HTTP: one process, a session per connection, each under the grant it presents
src/sdk/index.ts   the interceptor: policy decision, record, wrap(tool fn)
src/sdk/claude.ts  Claude Code command hook and Claude Agent SDK in-process hooks
src/sdk/openai-agents.ts, vercel-ai.ts, langchain.ts   framework adapters, tested against the real packages
src/sidecar.ts     the SDK issuer behind a local HTTP API, for agents in other languages
src/otel.ts        OpenTelemetry export: one OTLP/HTTP span per receipt, after the receipt, no SDK dependency
src/splunk.ts      Splunk export: one HTTP Event Collector event per receipt, token from the environment, beside or instead of otel
src/rest.ts        the REST connector: an HTTP API described as tools, standing where an MCP upstream stands
src/a2a.ts         agents delegating to agents: a remote A2A agent as an upstream, and the gateway serving A2A itself in front of it
src/upstream.ts    attested execution: an upstream signs its result for the receipt; the verifier checks it with the upstream key
src/capture.ts     private evidence: per-tool capture rules applied by the issuer to the whole statement, withheld values as salted commitments or encrypted under the customer's key, restore and reveal
vectors/           conformance vectors: receipts, keys, logs, proofs, and expected verdicts; `bun run vectors` regenerates them
src/verify.ts      offline verification, the human-readable report, and the audit that a later log extends an earlier one
src/cli.ts         keygen, grant, gateway, hook, serve, log, prune, verify, reveal, audit, policy simulate
src/retention.ts   pruning the log: leaves become their hashes, bundles are removed, proofs survive
src/index.ts       the package's public surface; adapters are exported on ./sdk/<framework> subpaths
tsconfig.build.json  emits dist/ (JavaScript plus declarations) for consumers; the repo itself runs the .ts directly
scripts/           fake Stripe upstream (signs its results with --key), a second fake upstream, fixture builders for gateway and SDK, the replay builder (`replay.ts`: example 27 and state example 07 as a folder of receipts and public keys anyone can verify with the published packages), demo
examples/          thirty-two runnable tutorials, plus examples/languages/: Python, Go, Java, and Rust clients of the sidecar, run by the test suite, one per aspect; each is run by the test suite
test/              unit tests per module, end-to-end gateway test, SDK and hook tests,
                   adapter tests against the real packages, and a test that runs every policy in docs/policies.md
docs/              tutorials, usage (gateway), codex-recovery (live demo), approvals, sdk, policies, verification

Plan ​

The design is two producers feeding one verifier. The SDK is the top of the funnel: cheap to install, wide reach, honest about being self-reported. The gateway is what a security or compliance owner mandates for consequential actions. Both exist; the work is widening each.

Done

  • Policy reload: the gateway re-reads policy.cedar whenever it changes and applies it from the next call, with no restart and no dropped session; a policy that does not parse is refused and the one in force stays, and every change or refusal is recorded in receipts/policy-reloads.jsonl (usage).

  • Live Codex support recovery demo: model-selected MCP calls, incorrect CRM memory, a reviewed recovery plan, simulated credit compensation, and verified evidence; guide, with offline regression coverage.

  • Payload-bound human approvals: short-lived signatures over the principal, agent, tool, full arguments, amount, currency, and beneficiary; configurable one- or two-key thresholds, enforced even in observe mode, a configurable lifetime cap, persistent atomic single-use consumption before forwarding, and independent approval-key verification. Guide, tutorial 30.

  • Gateway: MCP proxy, signed delegation, gateway-fetched facts, Cedar policy, denial receipts, Merkle log, offline verifier.

  • Receipt schema carries the issuer kind, so a verifier reads gateway versus SDK before anything else.

  • SDK core: policy decision, record, and a generic wrap(tool, fn) for any framework whose tools are functions.

  • Claude Code command hook for PreToolUse, PostToolUse, and PostToolUseFailure, with blocking on deny.

  • Claude Agent SDK in-process hooks over the same handler.

  • Provider-native deliveries: an upstream wrapping Stripe or GitHub attaches the signed webhook or delivery for the call; a verifier with the shared secret checks the HMAC, the timestamp, and the binding to the result, and reports the execution as attested by shared secret.

  • Logarithmic appends: the Merkle log caches complete subtrees, so issuing a receipt costs the same at the millionth leaf as at the first; measured at 0.15 ms per receipt and about half a millisecond per gateway call including policy, a fact lookup, and the upstream signature. A remote log adds one network round trip plus about five milliseconds of server work per call, two for a pre-committed call; the deployment guide has the measurements against the live log.

  • Retention on the log: prune replaces leaves older than a cutoff with their hashes and removes their bundles, so proofs still verify and the content is gone.

  • Several upstreams under one gateway and one grant, each tool owned by exactly one, with the receipt naming which served the call; consumed facts flow across them.

  • Attested execution: an upstream that holds a key signs its result for the receipt, the gateway embeds it, and a verifier given the upstream key reports the execution as attested rather than observed. The memory server and the demo upstream sign.

  • HTTP upstreams: the gateway reaches an already-running MCP server over Streamable HTTP with a bearer token from the environment, as well as spawning one over stdio.

  • Optional fact lookups: a lookup that references a call argument the call does not carry is skipped rather than denying, so policy can see the fact a write is about to supersede without refusing writes that supersede nothing.

  • Consumed facts: an upstream declares the facts it served in its result _meta, and every later gateway receipt in the session carries those ids as consumed, observed, so what the agent had been shown before each call is on the record.

  • The forwarded call carries the receipt id and the attested agent and principal in _meta, so a stateful upstream can cite the receipt; the memory server in @agent-custody/state runs this way.

  • Conformance vectors, generated by the test suite and published with the spec, and a browser verifier on agent-custody.dev that passes all of them.

  • Sidecar: the SDK issuer behind a local HTTP API (serve), with a Python package on PyPI-ready footing and Go, Java, and Rust clients, so agents in any language get the same receipts from one signing implementation.

  • Consistency proofs between tree heads (RFC 9162), served by the log and checked by the audit command, so an auditor holding an old tree head can prove nothing before it was rewritten.

  • Remote log: the issuer can append to a log run by someone else over HTTP, whose key then signs the tree heads, so a verifier learns the receipt was in a log the operator could not rewrite. Includes the reference log server, bearer-token auth, and a root endpoint for auditors.

  • Framework adapters, each tested against the real package with a scripted model and no network: OpenAI Agents SDK (wrapTools enforces, observeRunner records from lifecycle events), Vercel AI SDK (wrapTools over a real generateText loop), LangChain (ReceiptCallbackHandler records, issuer.wrap enforces).

  • Plans and the tenant portal: every tenant is on a plan (free, ten thousand appends a month; team, a million; enterprise, no allowance) enforced at append with a clear 429; the portal at the operator's PORTAL_HOST lets a team register, get its tenant and first key, watch usage against the plan, mint and revoke keys, buy the team plan through Stripe, and copy the export command, with every action in the audit trail.

  • An audit trail of administrative actions: every tenant created or disabled and every token minted or revoked is recorded with who did it, from the admin page or the command line, shown on the page and carried in the tenant's export.

  • A tenant's export: log-export takes, with the tenant's own token, every leaf hash, the signed head, the published keys, the checkpoints, and their usage, checks that they add up, and writes a log copy the verifier reads offline; the evidence never depends on the operator staying in business.

  • Monitoring and metering: log-check, the outside probe that verifies the head, the checkpoints, and the witness and exits 1 on trouble, run every ten minutes by the monitor workflow; GET /health; and usage per tenant per month on the admin page and as CSV.

  • The witness: a second signer on a machine the log's operator does not control countersigns each checkpoint after proving it extends the last one it signed, refuses a rewritten or forked history with an alarm, and publishes its key; audit --witness-url requires it. Phase 6 of issue #6.

  • The signer, keys, and checkpoints: the key in its own process (signer, --signer-url), the key document at /.well-known/agent-custody-log.json fetched and pinned by verify --log-url and audit --log-url, and signed checkpoints per log published to a directory and a table for a verifier who was not watching. Phase 3 of issue #6.

  • The log over Postgres: log --db-env, leaves as hashes in one table keyed by tenant, one writer per tenant by advisory lock, tenants and hashed tokens in tables managed by log-admin, rate limits and a body cap, retries in the sink, and import for an existing file log. Phase 2 of issue #6.

  • A log for someone else: hashOnly sends leaf hashes so the log never holds a receipt; the reference server runs several tenant logs at /t/<tenant>/ with their own tokens and ids; tree heads name their log and the verifier checks it with --log-id. Phase 1 of the hosted log, issue #6.

  • OpenTelemetry export: with otel in either config, every receipt is also one span at the collector the team already runs, trace id equal to the receipt id, attributes for tool, agent, principal, status, decision, and log position; after the receipt, best effort, never on the evidence path. A reconciliation receipt is marked and carries no policy decision, so a dashboard never counts it as a second allowed call.

  • Delegation chains for sub-agents: a grant that names the agent's key lets it delegate a narrower grant, with the parent embedded, up to three deep; the verifier and the gateway walk the chain to the principal, refusing any link that escalates scope, widens the window, changes the principal, or is signed by the wrong key; the receipt names the sub-agent and the principal and carries the chain. Pinned by the gateway-chain-* vectors and mirrored in the browser verifier.

  • One gateway for many agents: gateway --http serves the gateway over Streamable HTTP, one session per connection under the grant that connection presents, sessions sharing the upstreams and the policy and nothing else; a platform team runs one gateway in front of the tools instead of one process per agent.

  • Splunk export: with splunk in either config, every receipt is also one event at the HTTP Event Collector, with the receipt id, tool, agent, principal, status, decision, and log position as searchable fields and the token from the environment; the same best-effort rule.

  • The REST connector: a plain HTTP API described as tools in the gateway config, credentials from the environment, so an agent's direct API calls become receipted, policy-checked tool calls through the gateway.

  • Pre-commit authorization for consequential tools: named in precommit, a call is signed and logged before it is forwarded, withheld if the log will not take it, and its receipt carries the committed authorization with proof that it precedes the execution.

  • The grant on every call (issue #97): expiry and revocation are checked on every call and again before the forward, in every mode, on the MCP, HTTP, and A2A fronts; agent-custody revoke revokes a grant and every grant delegated from it. The remote log's answer is verified against pinned keys before a pre-committed call goes upstream (issue #99). The portal verifies an address only from the account's own session before a provider sign-in can link to it (issue #103). The verifier recomputes the execution result digest (issue #104). The Stripe webhook ignores a repeated or out-of-order event (issue #105).

  • Agent-to-agent delegation under custody: the A2A upstream and the gateway's A2A front; receipts per delegation with the delegate's card as a fact. A Google ADK RemoteA2aAgent, or any A2A client, is pointed at the gateway and a refused delegation comes back as a rejected task the delegate never saw. The delegate does not yet countersign its answer; the receipt attests what the gateway sent and observed, not what the delegate did with it.

  • Private evidence: per-tool capture rules, applied by the issuer to the whole statement so nothing outside a fixed structural list is in clear under a private rule; withheld values as salted commitments whose salts never enter the receipt, encrypted under the customer's key, revealed and recomputed by the verifier; the commitments do not confirm a guess of a withheld value. A human approval envelope carries the action fields and an unsalted argument digest, so a private rule withholds it as a commitment; the privacy limits apply.

  • Enterprise identity for the hosted portal: member roles (owner, admin, reviewer, readonly) with invites, TOTP MFA with single-use codes and an operator reset (log-admin user mfa-reset), and single sign-on per tenant through the tenant's own IdP, OIDC or SAML 2.0, set by the operator (log-admin sso set), with an enforce mode that closes the password, GitHub, and Google paths to members. Revoking an agent's authority mid-session already shipped (agent-custody revoke, example 27); example 31 shows the enterprise sign-in and the roles.

  • The state review console: search receipts by transaction, customer, agent or time, read the ten answers, and hand over a portable HTML/JSON case whose signatures, authorizations and inclusion proofs can be verified offline.

  • A log outage after an ordinary call (issue #149): the signed receipt waits in receipts/outbox/ and the gateway appends it when the log is back, the agent gets the real outcome marked log-pending, GET /health counts the pending receipts, and requirePrecommit refuses a remote log with an empty precommit. The failure table gives each case.

Next, in the order it pays off

  1. Run the witness for log.agent-custody.dev on a machine and under an account that is not ours, and require it in the welcome sheet. The code is done; what it needs is a second operator. Issue #6.
  2. Post-quantum signatures: ML-DSA beside Ed25519 in the same DSSE envelope, hybrid by default when a PQ key is present, in every signed artefact and in the browser verifier. Issue #11.
  3. Receiver attestation for agent-to-agent calls: the delegate signs its answer for the receipt, as a signing upstream does today.
  4. A TEE-hosted signer, then SD-JWT redaction, then ZK proofs of policy compliance. Not before.

A Python SDK follows the same shape once the TypeScript adapters have settled.