Changelog
All three packages, @agent-custody/receipts, @agent-custody/state, and agent-custody on PyPI, move in lockstep. The receipt format has stayed at v0.2 throughout; every addition to it is an optional field, so earlier receipts and the published conformance vectors remain valid.
0.10.0 — 2026-10-23
- Fix: the memory server and the review console stop on SIGTERM (issue #148).
agent-custody-memory serve --httpandagent-custody-memory reviewwaited for SIGINT only, so a container runtime, systemd or Kubernetes stopped them with SIGKILL after the grace period. They now close and exit 0 on SIGTERM. The memory server over HTTP answers GET with 405, because its held SSE stream kept a connected gateway from letting the stop finish, and it cuts the connections that remain 3 seconds after the stop starts. It closes the ledger before it exits. - Fix: a log outage after an ordinary call keeps its receipt (issue #149). Before, when the log did not take the receipt of a call that the gateway had already forwarded, the agent got an error and the receipt was lost, so a model could repeat a refund. Now the gateway keeps the signed receipt and its salts in
receipts/outbox/<id>.jsonand returns the real outcome with_meta["agent-custody/log-pending"]: true(LOG_PENDING_META_KEY). A serving gateway (gatewayover stdio,--http, or--a2a; the optionautoFlush) appends the outbox at start and every 30 seconds (GatewayHost.flushOutbox()), oldest statement first, and writes the bundle when the log takes it;checkandreconcilenever flush. A lock file in the outbox keeps several gateways on onereceipts/directory from appending a receipt twice, and the log's answer is recorded in the entry before the bundle is written, so a failed bundle write does not append the leaf again and is printed on stderr. An entry that another key signed stays in the outbox and is reported, not renamed. The entry is synced to disk before the agent gets the result.close()waits for a flush that runs.GET /healthreportspendingReceipts. Aprecommittool is still withheld when its authorization is not logged, and a financial operation keeps the old order. With a remote log and an emptyprecommit, the gateway prints a warning at startup;"requirePrecommit": truemakes it a startup error.docs/usage.mdhas a table of each log failure for each kind of tool. - Fix:
policy simulateskips a reconciliation receipt. A reconciliation receipt is a provider read, and no policy ran for it. A replay evaluated it as a secondstripe.refundcall, so a tighter refund policy reported a falseallow -> denychange and counted the refund two times. The replay now counts such a receipt in the new report fieldreconciliations. It does not count it inevaluatedorskipped, so the report stays complete.formatSimulationprints the count. The console's simulate response and page show the count too. A reconciliation whose original receipt is missing is a skip, so the report saysINCOMPLETE. The library treats a predicate as a reconciliation only when a gateway issued it and its operation names another receipt; it trusts the receipt only afterverifyBundle. - Fix: the
verifytext report shows a reconciliation receipt as a provider read. Thepolicyrow says that no policy was evaluated, and theexecutionrow starts withprovider read. The browser verifier prints the same rows. Before, the report printedpolicy observed allowfor a receipt whose reasons say that no policy ran. No check changes. - Gateway: reload the Cedar policy without a restart (issue #145). The gateway re-reads
policyFilewhenever it changes (device, inode, mtime, ctime, or size, and on each call while the file is under two seconds old), as it doesrevokedGrants, and evaluates the next call under the new policy, for stdio, HTTP, A2A, and observe mode, with no dropped session. A call already evaluating finishes under the policy it started with, and its receipt carries that digest. A changed file that does not parse, or cannot be read, is refused: the policy in force stays, never an allow, andPOLICY NOT RELOADEDgoes to stderr. Each change and refusal is appended toreceipts/policy-reloads.jsonlwith the time and both digests.GatewayHost.policyDigest()returns the digest in force. The console no longer tells the operator to restart after a save. - Fix:
policy simulatechecks the receipt id against the file name. A receipt copied over the file name of another receipt hid that call, counted the copy two times, and the report still saidCOMPLETE. The command now fails when<id>.jsonholds a receipt with another id, as it already does for an authorization file. A test also checks thatsimulatePolicyevaluates each receipt before it pulls the next.
0.9.0 — 2026-10-21
State: a live Codex support recovery demo (example 10; guide
docs/codex-recovery.mdin the receipts package). A Codex agent works through the gateway and writes a wrong benefit and a credit, because of a wrong CRM plan. The operator corrects the plan.inspecttraces the blast radius and plans the retractions, the corrective writes, and the credit reversals.recoverruns the plan only with an approval that equals the digest of the saved plan, andverifychecks the stores against the receipts.inspectrefuses a credit or a current benefit that no memory read supports, because the demo cannot decide its recovery. After a partial recovery, a refreshed plan lists the corrective writes again.State, breaking for custom stores:
Store.remove(externalId, fact)is nowdelete(entry), andverifyRemoved(externalId, fact)is nowverifyDeleted(entry), whereentryis aStoreEntry { externalId, factId, text, storedAt? }.createMemoryServerrefuses a store that has nodelete. A store written for the earlier interface therefore fails at startup. It is never called with an entry where it expects an id, so its old check can never certify an erasure that did not happen. The adapters in this package are changed already.entryOf(fact, externalId)builds an entry from a fact.State: the repair sweep (issue #107). A write reaches the stores before the ledger, so a failed ledger append leaves store entries that the ledger has no fact for.
memory.repairandagent-custody-memory repair --via <gateway.json> --reason <text>find those entries through the new optionalStore.list(), which every adapter implements. The sweep removes each entry that is more than ten minutes old, verifies the removal, and records arepairledger event under the entry's fact id. The ledger records the event before the sweep removes the entry, so the first record of a fact id wins:Ledger.assertrefuses a fact id that has a repair event, andLedger.repairrefuses one that has an assert or returns the event that the entry already has (within one server process). Onememory.repairruns at a time in a server process. Each store entry recordsstoredAt. A write whose stores take more than five minutes is refused before its ledger append, so the entry of a write in progress is not old enough to repair. If the ledger append stalls for longer, the first record wins and the other fails with an error. This holds within one server process. For servers that share a Postgres ledger, an append that stalls after its check can still land after a repair. Example 09.The gateway console.
agent-custody console --config gateway.jsonis an admin service for the person who runs a gateway, separate from the gateway and behind a token read from the environment: the config, the upstreams and tools, the config check, the newest receipts with a verdict each, a policy editor that validates and replays the recorded calls against the proposed policy before it saves (the old version is kept, a stale edit is refused, and the change is audited), grant inspection and revocation, and the audit trail. It holds no principal key and signs nothing; its page writes every value withtextContentunder a hash-only content-security-policy. The gateway image runs it withconsole(compose profileconsole), with its token as a container secret that only the console service holds and the same user as the gateway. A saved policy is guarded: a permit rule with no action and no condition, which a replay of past calls cannot show, needs confirming, and the digest check, the history copy, and the write happen under one lock. The console does not hold the gateway's credentials, so it does not check them or start upstreams that need them, and a connect check that runs too long is cancelled and closes what it started (createGatewayHosttakes asignal). Tutorial 32;docs/console.md.Docs and tooling: configuring the gateway.
docs/configuration.mdis the map of what is configured where (the grant, the Cedar policy,gateway.json, the credentials), how a call is decided, tool naming, a worked example with two agents on one shared gateway and four kinds of upstream, how the config works in the Docker image, and a table of startup errors. A test checks that its example config parses, that its policy parses, and that the policy does what the prose says for each agent.agent-custody check --config gateway.json [--connect]finds what a config would fail on at startup before it starts: unknown keys (a typo such asprecomitwas silently ignored), a policy that does not parse, missing credential variables or stdio commands, paths that cannot be written, an expired grant, and, with--connect, two upstreams offering one tool and tool names that no upstream offers.GatewayHost.tools()lists the tools the upstreams offer. The gateway image runs the check on every start and does not start on an error (GATEWAY_CHECK=advisoryoroffto change that).deploy/gateway/exampleis a config directory to copy (a REST API, a policy, a fact lookup,precommit), with the steps to make the principal's key and issue grants; a test copies it, drives it against a stand-in API, and checks that the policy, the fact lookup, and the grant scopes refuse what they should. The configuration guide says how to make, keep, distribute, rotate, and replace the principal's key: the private half stays where grants are issued, and the gateway holds only the public half.checkalso reports a REST tool withidempotencyHeaderand nooperationsentry, which makes every call to it fail. An unknown config key, and aprecommitname that no upstream offers, are errors, socheckexits 1 for them; apermit(in a comment does not count as a permit rule; the Splunk and OpenTelemetry credentials and the operations database and approvals directory are checked. The example and guide policies bound a refund amount from below, and the example says what a retry does withoutoperations.Deploy: the gateway as a service.
deploy/gatewayis a container image, compose file, Kubernetes manifests and systemd unit for one shared gateway that every agent on a host or cluster connects to, each with its own grant. Thegateway-imageworkflow builds it for amd64 and arm64 and pushesghcr.io/svayatta/agent-custody-gateway:edgeandsha-<short>on every merge to main,<version>,<major>.<minor>andlateston every release. Every build is smoke-tested before it is pushed.initwrites a starter config, an example policy, and a demo grant.Deploy: a release deploys the release.
upgrade.shchecks out the tagv<version>before it deploys, so the compose file, the Caddyfile, and the scripts on the host are the released ones and not main's, and it restores the previous checkout on a rollback. TheAGENT_CUSTODY_VERSIONdefaults in the compose files, the Dockerfile,.env.example, and the Kubernetes manifest, and the monitor's pinned version, are checked against the package version on every pull request and again on the release tag. A pull request that touchesdeploy/builds the log image from the workspace package, starts it, and checks the compose files, the manifests, the scripts, andupgrade.shagainst a throwaway repository.Supply chain: signed, attested, pinned. Every pushed image (log and gateway) carries a provenance attestation and an SBOM and is signed with cosign, keyless; the digest of each release is in the workflow's step summary. Base images are pinned by digest, third-party GitHub Actions by commit, and bun by version, with Dependabot proposing updates. The monitor runs the pinned release, not
@latest.Fix: long-running services stop on SIGTERM. The gateway, log, signer, portal and witness waited for SIGINT only, so a container runtime, systemd or Kubernetes stopped them with SIGKILL after the grace period. They now close and exit 0 on SIGTERM.
Receipts: enterprise identity for the hosted portal (issue #115). Roles: each member of a tenant is
owner,admin,reviewer, orreadonly.readonlyreads,revieweralso publishes and removes policy versions,adminalso manages API keys, invites, and members below owner (never an owner, never grantingowner), andowneralso manages billing. A write route that no role table lists needsadmin. The last owner cannot be removed or demoted. New routes:GETandPOST /api/members,POST /api/members/<userId>/role,POST /api/members/<userId>/remove,POST /api/invites/remove, andPOST /api/invites/accept, which needs a verified address and allows one tenant per account./api/mereturnsrole,mfa, andinvites. MFA: an account can turn on a time-based one-time password (RFC 6238, 6 digits, 30 seconds) withPOST /api/mfa/setup,/api/mfa/enable, and/api/mfa/disable.POST /api/loginthen answers{ "mfa": true }with a five-minutecustody_mfacookie and no session,POST /api/login/mfafinishes the sign-in, and GitHub and Google sign-in go through the same code step. A code works once, and wrong codes are throttled per address and per account.agent-custody log-admin --db-env NAME user mfa-reset <email>turns MFA off for an account that lost its authenticator.user mfa-reset,sso set, andsso removewrite the same audit entry as the otherlog-admincommands (user.mfa-reset,sso.set,sso.remove, with the actorcli:<user>@<host>; never a secret). Single sign-on per tenant, set by the operator withlog-admin sso set|show|remove: OIDC (authorization code with PKCE; theid_tokensignature is checked against the IdP's key set, RS256 and ES256 only) or SAML 2.0 (service-provider initiated, signed assertions required, through the optional peer dependency@node-saml/node-saml, loaded when first needed). An IdP is trusted only for its tenant's email domains, and a domain belongs to one tenant. A sign-in creates or links the account, joins it to the tenant with the default role (an unverified password account with that address is deleted with its password, sessions, memberships, and linked providers, whether or not it created a tenant, so a squatter cannot lock the member out; a member that an admin removed rejoins through single sign-on only after a new invite), and skips the portal's MFA, because the IdP owns it. With--enforce, members cannot sign in by password, GitHub, or Google, their older sessions end, and invites are refused. Routes:GET /auth/sso?email=,GET /auth/sso/<tenant>,GET /auth/sso/<tenant>/callback,GET /auth/sso/<tenant>/saml(metadata), andPOST /auth/sso/<tenant>/saml/acs. New modulesrc/sso.ts, example 31, and the Dockerfile installs@node-saml/node-saml@5besidepg. The gateway kill switch was already covered:agent-custody revokeandrevokedGrants(example 27); a test now also shows that another agent continues after one agent's grant is revoked.Supply chain: cosign 3 signs the images. The image workflows install cosign with
sigstore/cosign-installerv4, which installs cosign 3. An image signature is now a Sigstore bundle, kept as an OCI 1.1 referrer of the image. Verify an image with cosign 3 or later.State: the review console and offline case files (issue #113).
- Review console:
agent-custody-memory review --receipts <dir>serves an index of receipts with search by transaction and customer id, or writes a static copy with--out. A record that cannot be verified, or whose file name does not match its receipt id, is listed as NOT VERIFIED with the reason, never left out. - Case files:
review --case <id>exports one receipt as a case file (.case.jsonor a self-checking.case.html), with the ten answers about the call.review --verify-casechecks either form against independently supplied keys, including--approval-keyfor human-approved calls. It reads a file as JSON only when its name ends in.json, and requires a page to be exactly what its case data renders.
- Review console:
Receipts, verifiers: both verifiers now refuse, in
structural parts in clear, a receipt without its policy digest, an authorization without its proof and tree head, or an attested principal without its key id. No receipt the code produces lacks them; four vectors cover the refusals.State: a live Codex support recovery demo. Example 10 and
examples/codex-recovery/let a Codex agent make a wrong refund decision on a stale belief. They then show the operator's recovery: revoke, inspect the blast radius, retract, write the corrected values, reverse the credit, and verify. The tests drive it with scripted calls and never call a model.Docs: the architecture guide (
site/guide/architecture.md): what runs on the customer's side and what the hosted log holds, the data model, and sequence diagrams. It states that no independent witness runs for the hosted log yet.Release: the image and MCP Registry jobs wait until npm serves the released version, so they no longer race the publish.
0.8.0 — 2026-10-19
- Receipts: human approvals (#111).
approvalsin the gateway config names tools that need a signed, unexpired, single-use human approval, in every mode.- Binding: the approval is bound to the principal, the agent, the tool, the amount, the currency, the beneficiary, and a digest of every argument. One or two distinct approvers are required, and
maxLifetimeSecondscaps the lifetime (default 3600). - Single use: consumption is atomic in a
usedDirthat gateway processes share across restarts. - Checked again before forwarding: expiry or revocation during fact lookups or precommit refuses the call with a signed receipt.
- Evidence: receipts and precommit authorizations carry the approval, and both verifiers check its signatures (
--approval-key), validity window, payload binding, consumption, and precommit consistency. - Privacy: under a private capture rule the approval envelope is withheld unless every argument is in clear and the tool name is not masked. A refusal carrying a valid approval still verifies.
- Tutorial 30.
- Binding: the approval is bound to the principal, the agent, the tool, the amount, the currency, the beneficiary, and a digest of every argument. One or two distinct approvers are required, and
- Receipts: financial operations (#112).
operationsin the gateway config makes tools financial.- No second execution: each call keeps one caller-chosen operation UUID, sends the provider an idempotency key, and reserves its amount against a daily budget in SQLite transactions shared across processes. A retry or a restart never forwards a second payment.
- Outcomes: submitted, pending, unknown, confirmed, or failed, recorded apart from execution.
- Reconciliation:
agent-custody reconcile --config gateway.json --operation <uuid>(GatewayHost.reconcile) reads the provider's record and closes an unknown or pending outcome with a signed reconciliation receipt. A call the gateway can prove it never sent is closed and its reservation released. Recovery identifies a dead creator by PID and start time, and the authorization save refuses a call that recovery has closed. - Verification and export: both verifiers check the operation linkage, and OTel, Splunk,
explain, andreviewmark reconciliations. - Tutorial 29.
- Receipts: policy simulation (#114).
agent-custody policy simulate --policy <proposed.cedar> --receipts <dir> --issuer-key …andsimulatePolicyreplay recorded calls against a proposed Cedar policy.- Trust: every receipt is verified first, then replayed on its recorded arguments, facts, and grant.
- Report: each changed decision is listed with its receipt id.
- Incomplete runs: a call the replay cannot evaluate makes the run INCOMPLETE. That covers a gate refusal, withheld inputs, and an authorization whose receipt never arrived.
- Flags:
--approval-key,--capture-key-env, and a repeatable--disclose. - Memory: receipts stream one at a time.
- Tutorial 28.
- Receipts: other changes.
- Report: the verify report shows why a call was refused (
denied: delegation grant has been revoked), or says the reason is withheld under a capture rule. - Example 27: revokes an agent's grant mid-session.
- Replay builder:
scripts/replay.tspackages example 27 and state example 07 as a folder of receipts and public keys that anyone can verify with the published packages.
- Report: the verify report shows why a call was refused (
- State: example 07 lists a call whose consumed beliefs are withheld under a capture rule as INCOMPLETE in a blast radius. With the customer's key, the same query finds the call.
- Site: the landing page leads with the local refund demo and shows its six steps (#117).
- Release and deploy:
- Automatic publish: a version tag publishes the packages from CI (
release.yml, trusted publishing, oncePUBLISH_FROM_CIistrue). The log image and the MCP Registry listing follow only after the release succeeds, so the image is never built before its package exists. - Manual deploy:
deploy/upgrade.shupgrades the log's host to a published version and rolls back if the new one is not healthy, or withdocsrebuilds the docs site. Thedeployworkflow runs it over SSH, by hand, with a key that can run nothing else; seedeploy/README.md.
- Automatic publish: a version tag publishes the packages from CI (
0.7.0 — 2026-10-15
- State:
blastandloadReceiptsknow what they cannot see: a receipt that withholds its consumed fact ids under a capture rule is listed underwithheld(INCOMPLETEin the report) rather than counted as having relied on nothing, and--capture-key-env/loadReceipts(dir, { captureKey })decrypts the ids of receipts in modeencrypted;explainsays when the beliefs shown are withheld. - Receipts: private evidence (issues #109, #76, #126).
capturein the gateway and SDK configs says, per tool or"*", what the receipt file carries of the arguments, the result, and the facts:clearas before,digestkeeps the fields listed inclearand withholds every other top-level field as one salted sha256 commitment at$rest(so that field names, which the agent or the upstream chose, never appear either),encrypteddoes the same and carries the object under a 32-byte key from the environment variablecaptureKeyEnvnames (AES-256-GCM; a rule in that mode without the key fails at startup). The rule is applied in one place:createIssuertakescapture: { rules, key }andprivateViewturns the whole predicate into the statement it signs, the authorization included, before the log and the exporters see it. Once any side is private, a side the rule does not set is withheld whole, and so is every value that is not structural (policy error messages, a denial reason, a thrown message, the result's_meta, a provider delivery's body, a field added later), in the rule's mode; a fixed list stays in clear (the receipt id, key ids, timestamps, the host's session and model ids, the tool, the grant, the digests, the decision and policy ids, the status, the provenance fields, a provider's signature and bind path, the note); a provider's delivery body, an upstream's signed envelope, the result's_meta, and the consumed fact ids are upstream-authored and withheld under any private rule, beside a clear result too. On a resultisErrorstays in clear and_metanever does. The policy decides on the real values; the agent gets the real result, error, and reason; the digests and the signature are over the whole call, and no salt is in the receipt: a commitment is{ $custody: "withheld", sha256 }, its salt kept by the issuer inreceipts/salts/<id>.json(0600 in a 0700 directory, pruned with the receipt) for a digest-only commitment, or inside the ciphertext with the value for an encrypted one, and nowhere else;request.argsDigest,execution.resultDigest, and each fact'sresultDigestare salted from the salts of the values withheld under them. So nothing in the receipt confirms a guess of a withheld value (#126; the dictionary regression intest/capture.test.tsruns in both modes). A disclosure carries{ $custody: "disclosed", salt, value }at each path, asrevealwrites it; a value without its salt is refused, and so is a salt without its value, whichreveal --include-saltswrites for the holder to complete and never by default. The salts file is written 0600 in a 0700 directory and pruned with the receipt. A verifier skips an upstream check only for a part the signed note declares withheld (vectorgateway-undeclared-envelope-commitment-resigned), and both verifiers require string hash fields (vectorssdk-array-hash-commitment-resigned,sdk-array-keyid-commitment-resigned). One function,effectiveRule, selects a call's rule for the gateway and the issuer alike; beside a result left in clear an envelope that is exactly the plain attestation for this call stays (every envelope and signature-entry field checked, canonical bytes, bound to this receipt, tool, and result) and a withheld one fails the upstream check (vectorresigned-clear-result-envelope-withheld); the upstream checks run beside a clear result whose only withheld part is_meta(vectorsgateway-private-clear-result-upstream-attested,gateway-private-clear-result-upstream-wrong-key); a key that is not a commitment's fails the checks naming the key id; the browser checks iv and tag lengths (vectorgateway-private-short-tag-resigned); a note head that is a prototype member fails instead of throwing (vectorresigned-note-path-proto); a fact argument named$restis an argument;loadReceiptsskips authorization files;explainopens the explained receipt's consumed ids with the key and prints the beliefs it opened. The provider's bound value must appear in what the agent received, never in the delivery's copy in_meta(vectorresigned-provider-bound-value-missing); for a tool no upstream offers the issuer combines the name's rule and the wildcard side by side, the more private mode and only the fields both keep in clear; the issuer checks the salts directory before signing and keeps an authorization's view until the log took the receipt;revealfails on a foreign key; the Splunk event never carries a commitment;explainand the packs report withheld consumption asINCOMPLETEand take--capture-key-env; a note naming a path twice,execution.upstreamwhole, orexecution.result.isErroris refused. An encrypted commitment is bound to its receipt and path (AES-GCM additional data),revealtakes--issuer-keyand verifies the bundle before decrypting, writes--outowner-only and never through a symlink; both verifiers runstructural parts in clearfirst, refusing a note that is malformed or names a structural part, and a statement whose status, tool, grant, or digests are not what the checks read (five vectors); a provider's signature and bind, the session and model ids, and a tool name no upstream offers are withheld under a private rule (the rule is selected by the real name, the view shows(unknown tool)),consumedis shared with the authorization,pruneremoves the authorization file, the issuer writes salts after the log append and lets an authorization's view go after an hour, the key is accepted in strict hex or base64 only, and an encrypted commitment needs a 12-byte iv and a 16-byte tag. A fact'sresultDigestis a commitment to the lookup's full result under a private rule (disclosed, decrypted, and checked atwithheld facts.<name>.resultDigest), since the receipt shows only the selected value and the rest of the result is private whatever the selected fields are; a producer that does not pass the full result gets the digest string withheld;reveal --outwrites owner-only and chmods an existing file; a result'sisErrorstays in clear only when it is the boolean flag; a tool or fact named like an inherited property (constructor) is looked up by own property; a receipt id from a bundle or a log line is checked before it becomes part of a path; the issuer keeps an authorization's view only once the log has taken it, bounded, and builds the receipt's view on it rather than withholding the shared parts twice. An upstream's signed envelope is withheld with the result, since its payload may carry any claim; a commitment is recognised only when well formed (vectorsdk-malformed-commitment-resigned); the OTLP span's status message is a fixed string where the error is a commitment. The statement carries one note,capture: { mode, withheld: Path[] }, naming every path that holds a commitment; a verifier trusts the note, not the shape of the data.verifyBundlerestores every path it can (VerifyOptions.disclosed, a partial predicate with{ $custody: "disclosed", salt, value }at each withheld path;VerifyOptions.captureKey), recomputesrequest args digestandexecution result digestfrom restored values and passes them as committed otherwise, reportsfact <name> value,fact <name> args, andwithheld <path>for the rest, refuses a disclosure for another receipt (disclosure names this receipt), and fails any path the note names without a commitment or a disclosure that contradicts one.agent-custody verify --disclose <file> --capture-key-env NAMEandagent-custody reveal <bundle> --capture-key-env NAME [--out file], which writes the disclosure in that shape and lists the paths it cannot reveal. The browser verifier mirrors the committed case. The Splunk event says "(withheld under the capture rule)" where a message is a commitment.src/capture.ts(privateView,restorePaths,getAt,setAt,ruleMode), example 26,test/capture.test.ts, andtest/capture-canary.test.ts: a secret in the withheld arguments, an upstream (scripts/fake-canary.ts) that repeats it in every place a result can carry a value, and every path through the gateway (executed, failed, thrown, denied, a lookup answering isError, a lookup that throws, out of scope, withheld, with a pre-committed authorization) and the SDK (recordon every outcome,wrapwith a throwing tool, which every adapter goes through), asserting that the decoded statements, the authorization, the files, the log, and the exporters carry none of it, with a control that shows it without a rule. Vectors regenerated: 51 cases. The receipt format stays v0.2: the fields are optional. The design followed four review rounds by 0xAnto on #123, each of which found a path a per-field design had left in clear (fact lookup arguments, a provider delivery body, a renamed fact argument, a failed lookup's answer, thrown errors, two facts sharing a name, SDK errors), which is why the rule now applies at one choke point with an allowlist and the canary test runs every path. - State, Python: unchanged; released in step.
0.6.20 — 2026-10-14
- State: security fix (issue #95, P0 in the 14 October review). The memory server believed the
_metaof any caller holding the shared token, so a direct writer over HTTP could write a fact asattested, or asverifiedwith an invented observation, under an invented actor, even with gateway calls required. Now the token decides:serveMemoryHttptakestokens(the gateways') anddirectTokens(the direct writers', required whenrequireGatewayis false), the CLI takes--direct-token-envbeside--token-envand refuses--allow-directwithout it, and a direct caller's_metais dropped before the server reads it (createMemoryServer'strustMeta, default true exactly whenrequireGatewayis not false, so over stdio--allow-directmeans the one client is not a gateway). A server given one token for both roles refuses to start, and so does an HTTP server without a gateway token (tokensis required, andserve --httpneeds--token-env), which before took any caller for a gateway. Also the actor and receipt of a call now travel as a value through forget and sweep instead of shared mutable state, so concurrent calls cannot record each other's actor. Regression tests intest/http.test.tsandtest/server.test.ts. - Receipts: the P1 fixes from the same review. The grant is enforced on every call, in every mode, and again before the forward: a session whose grant expired refuses its next call with a denial receipt, observe mode suspends the Cedar policy and nothing else, an HTTP session under an expired or revoked grant is closed on its next request, grants can be revoked (
revokedGrantsfile, written by rename,agent-custody revoke,GatewayHost.revoke) by the digest of the signed payload, so a repacked envelope stays revoked, and revoking a grant revokes every grant delegated from it; a revocation file that cannot be read denies every call; the A2A front's other methods (GetTask,CancelTask) need a grant in force too; the verifier passes a denied or withheld receipt issued after the window, since only an executed call must sit inside it (issue #97). The remote log's answer to every append is verified before a pre-committed call is forwarded and before any receipt is written: tree head signature against the log's pinned keys (log.keys, else its key document fetched once),log.logId, proof size, and inclusion of that very leaf; a gateway config withprecommitand a remote log must pinlog.keys, since keys fetched from the log itself vouch for a log dishonest from the start; the SDK config now resolveslog.keysagainst its own directory (issue #99).canonicalizekeeps an own property named__proto__in the digest (issue #102). The portal sends a verification link at registration, never links a provider sign-in to a password account that has not verified its address, verifies an address only from that account's own signed-in session (POST /api/verify-email; the emailed link alone verifies nothing), and links a provider from a signed-in session with?link=1(issue #103). Vectors regenerated: 31 cases, withgateway-executed-proto-keyandgateway-denied-expired-grant. - Receipts: two P2 fixes from the same review. The verifier recomputes the execution result digest of an executed or failed receipt (
execution result digest, inverify.tsand the browser verifier), so an issuer that signs a wrong execution result digest no longer passes; new vectorresigned-result-digest, 32 cases (issue #104). The portal's Stripe webhook records the event ids it applied and ignores a redelivery, and ignores an event older than the one that set the stored billing state, so a repeated or late event cannot move a tenant's plan back; an event withoutidandcreatedgets 400 (issue #105). - Python: unchanged; released in step. The Python suite's memory server runs with a direct token.
0.6.19 — 2026-10-10
- Receipts: agents that delegate to agents. A fourth upstream shape,
{ "a2a": { "url", "headers"?, "headerEnv"?, "timeoutMs"?, "prefix"? } }, is another agent reached over the A2A protocol: the gateway fetches its card at startup and offers<prefix>.send, which delegates a task, and<prefix>.card, the card as a fact a policy decides on; both the 1.0 and the 0.3 wire formats, the card choosing; a streamed delegation relayed once the agent finishes, with the final task on the receipt;precommitas for any consequential tool. Andgateway --a2a, the gateway as an A2A agent in front of that upstream: it serves the remote agent's card at its own address, turns everySendMessageinto a<prefix>.sendcall under the config's grant or the one in the request's grant header, answers an allowed delegation with the remote agent's task carrying the receipt id inmetadata, a refused one as a rejected task with the reason and the receipt id that the remote agent never heard of, and forwards every other method untouched. A Google ADKRemoteA2aAgentpointed at the gateway's card needs no other change. Example 25 andtest/a2a.test.ts, with stand-in agents in both formats. - Python: a Google ADK adapter,
agent_custody.adk:adk_callbacks(client)givesbefore_tool_callback, which records the denial and returns the error dict naming the receipt so ADK skips the tool and the model sees why, andafter_tool_callback, which records the executed call. Extraadk(google-adk[a2a]). Tested against ADK's own runner with a scripted model intests/test_adk.py, andtests/test_adk_a2a.pyruns ADK'sto_a2aremote agent andRemoteA2aAgentthroughgateway --a2a, both receipts verified. - State: unchanged; released in step.
0.6.18 — 2026-09-29
- Receipts: the founder's note.
agent-custody portal-followup, run hourly from cron (deploy/followup.sh), sends every registration between an hour and a week old a personal note fromMAIL_FROM, once: which stack, an offer to do the integration, start in observe mode.PortalStore.pendingFollowUps/markFollowedUp(portal_users.followed_up_at),founderNote,sendFollowUps, anddeliverMailshared with the portal's own mail. Accounts on the operator's own mail domains are skipped; a provider failure is retried next run. - State, Python: unchanged; released in step.
0.6.17 — 2026-09-29
- Receipts: a DeepSeek Harness plugin,
@agent-custody/receipts/sdk/deepseek-harness. The module is the plugin: list it by its package path with the SDK config file as its setting.tools/pre-executedenies an enforced policy deny with the receipt id in the reason the model sees and delegates otherwise;tools/post-executerecords the executed or failed call with the harness's session and call ids. Typed from the harness's own tool types with no dependency; driven as the harness drives it in example 22 and a test. The harness's Claude Code hooks bridge runs thehookcommand unchanged as the out-of-process alternative. - State, Python: unchanged; released in step.
0.6.16 — 2026-09-28
- Receipts: observe mode.
"mode": "observe"in the gateway or SDK config forwards every call the grant allows and records the decision the policy would have made, withenforced: falseon the receipt'spolicy; a deny is recorded, not acted on, in the gateway,wrap, and every hook adapter (Claude Code, Claude Agent SDK, OpenAI Agents, Vercel AI, OpenClaw), and the sidecar carries the flag so the Python adapters and Hermes plugin do the same. The verifier's consistency check accepts a not-enforced deny beside an executed call and names the mode; a receipt with the flag stripped fails. Two new conformance vectors,gateway-observe-deniedandresigned-observe-flag-stripped; the browser verifier mirrors the check. The receipt format stays v0.2: the field is optional. - Python:
denies(policy)tells an enforced deny from a recorded one; the adapters use it. - State:
explainsays when a call went through because the policy was in observe mode.
0.6.15 — 2026-09-28
- Receipts: an OpenClaw adapter,
@agent-custody/receipts/sdk/openclaw:openclawHooks(issuer)givesbefore_tool_call, which blocks a policy-denied call with the denial receipt's id in the reason and returns nothing otherwise, andafter_tool_call, which records the executed or failed call;registerOpenClaw(api, issuer)wires both in a plugin'sregister(api). Driven against OpenClaw's documented hook types in example 21 and a test. - Python: a Hermes Agent plugin,
agent_custody.hermes:pre_tool_callblocks a denied call with the receipt id,post_tool_callrecords;register(ctx)is the plugin entry, andpackages/python/hermes-plugin/is the drop-in directory with itsplugin.yaml. Tested against the real sidecar. - State: unchanged; released in step.
0.6.14 — 2026-09-28
- Receipts: registration in two steps: an account from an email and a password, or from a GitHub or Google sign-in, then the tenant on the next screen (name, company, tenant id, role, phone, Telegram), with the first key and the emails once the tenant exists.
POST /api/registerwith only email and password creates the account;POST /api/onboardcreates the tenant; the one-call register with everything still works for scripts.GET /auth/githubandGET /auth/googlerun the OAuth web flow over fetch with a signed state cookie; a provider sign-in links to the account with the same verified email or creates one (portal_identities).--github-client-id/--github-secret-env,--google-client-id/--google-secret-env;GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRETin the container. - State, Python: unchanged; released in step.
0.6.13 — 2026-09-28
- Receipts: published policy versions. A tenant publishes each Cedar file it deploys under a name; the digest is the same SHA-256 the gateway writes into receipts as
policyDigest, so a receipt's digest reads as a name on the portal's new Policies page, in its match-a-receipt tool, and in the export'spolicies.json.PostgresTenancy.addPolicy/listPolicies/policy/policyByDigest/removePolicy, audited aspolicy.addandpolicy.remove;GET /t/<tenant>/policieswith the tenant token; portal routes under/api/policies. Enforcement is untouched: the gateway never reads any of it. - State, Python: unchanged; released in step.
0.6.12 — 2026-09-28
- Receipts: listed in the MCP Registry as
io.github.svayatta/agent-custody. The package carriesmcpName, which the registry checks against the published npm package, andpackages/receipts/server.jsondescribes the entry: the gateway over stdio, run asnpx @agent-custody/receipts gateway --config <gateway.json>. The repository moved to thesvayattaorganisation; links and the container image (ghcr.io/svayatta/agent-custody-log) follow it. - State, Python: unchanged; released in step.
0.6.11 — 2026-09-27
- Site: a logo: a receipt with a torn edge, two ledger lines, and a tick, on the amber tile;
logo.svg,wordmark.svg, and the PNG icons. The home page is laid out along the lines of dunetrace.com: a centred hero with three actions (Register for a free account, View GitHub, Verify a receipt) and the film under them, then the problem, how it works, denied at runtime, not a trace, a quick start with the commands, and where to go. - Receipts: the portal and the Caddy hosts carry the new mark;
app.agent-custody.dev/#registeropens the portal on the registration form. - State, Python: unchanged; released in step.
0.6.10 — 2026-09-27
- Receipts, fix: the portal and the admin page were blank on 0.6.8 and 0.6.9: a quote escaped once inside the page's template literal made the inline script fail to parse. Fixed, and both tests now compile every inline script. The pages' Content Security Policy allows the favicon (
img-src 'self'). - State, Python: unchanged; released in step.
0.6.9 — 2026-09-27
- Receipts: the portal and the admin page have a dark mode switch (a choice kept in the browser; unset, they follow the system) and carry the mark as their favicon; the portal serves
/favicon.svg. - Deploy: Caddy serves the mark as
/favicon.svgand/favicon.icoon the log, checkpoints, portal, and admin hosts, and the log and checkpoints landing pages link it. - Site: the home page has a light and dark switch in its top bar, sharing the docs' setting.
- State, Python: unchanged; released in step.
0.6.8 — 2026-09-27
- Receipts: the portal's welcome is five numbered steps with copy buttons (keep the key, point the gateway at the log, send the first receipt, what to hand a verifier, take your data), the log's addresses, and what the log does not do, instead of a monospaced sheet; the same page is under Setup in the dashboard whenever it is needed again.
/api/registerand/api/overviewcarry it assetup; the textwelcomestays for scripts. - Receipts: registration sends mail: a welcome to the person with the setup sheet and never the key, and, with
--mail-notify, a note to the operator with the contact details. Through Resend's HTTP API with no dependency (MailOptions,--mail-key-env,--mail-from,--mail-notify;MAIL_API_KEY,MAIL_FROM,MAIL_NOTIFYin the container). Best effort: a provider failure is logged and never fails a registration. - State, Python: unchanged; released in step.
0.6.7 — 2026-09-27
- Receipts: registration asks who is registering: name and company or organisation are required, role, phone, and Telegram username optional; the tenant id is filled in from the company name, explained as the path of the log, and previewed as the full URL before the form is sent.
PortalStore.createUsertakes the profile;portal_usersgains the columns on start. The admin page's Registrations section and/admin/registrations[.csv]show who and how to reach them. - State, Python: unchanged; released in step.
0.6.6 — 2026-09-27
- Site, #52: the home page leaves the docs theme: its own top bar with five entries (Product, Docs, Pricing, Verify, Sign in), one story, the real receipt with the one amber action, one sentence on what a receipt does not prove, three doors (Try it, Make it evidence, For security review), a flow diagram, an in-page demo of the gateway and the verifier, and a comparison with application logs and traces. The docs nav is the same five entries; Receipts, State, Python, and the spec are in the sidebar. Getting started opens with the stacks (Claude Code, Claude Agent SDK, OpenAI Agents SDK, LangChain, Vercel AI, Python) and says which path is the agent's word and which is evidence; the trust table moved to the pieces page. Pricing says what 10,000 appends is in a worked week.
- Receipts, #52: the portal overview states the trust limit of the plan: heads signed by our key, no second witness yet, with a link to the proof table.
- State, Python: unchanged; released in step.
0.6.5 — 2026-09-27
- Site, #52: the home page is one column: the claim, one action, and a real receipt from this repository's custody (the denied
git push --force) with a control that opens it in the browser verifier, then the six steps as a numbered sequence. The VitePress hero and feature grid are gone; the docs keep the theme./verify?receipt=<id>loads one of the site's own receipts with its keys and verifies it on arrival. - Receipts, #52: the tenant portal wears the site's mark and amber in its header, so registering reads as the next page of the same site.
- State, Python: unchanged; released in step.
0.6.4 — 2026-09-27
- Receipts: the admin page opens with a Registrations section: who registered each tenant through the portal, plan, billing state, appends in the chosen month, leaves in total, live tokens, with totals across tenants and a CSV.
GET /admin/registrations?month=and/admin/registrations.csvbehind the admin token;PortalStore.registrations()reads the portal's tables; the log server wires the portal store in when it runs on Postgres. - Deploy: the admin page on its own host,
ADMIN_HOST, mapped by Caddy onto the log service's/adminsurface with the token still required on every request./adminon the log host is unchanged. - State, Python: unchanged; released in step.
0.6.3 — 2026-09-27
- Site, #49: relative links written without
.md, as the reference pages write them, resolve to the page instead of becoming repository URLs that do not exist; about sixty links across ten reference pages were dead. The docs host's root goes to the guide. The home page's install block says which line is the command line and which the Python package. - Receipts, #49: the portal's top bar links to the guide, the reference, and the browser verifier.
- State, Python: unchanged; released in step.
0.6.2 — 2026-09-27
Six vulnerabilities reported as issues #41 to #46, all fixed here.
- Receipts, #42: arguments reach Cedar as data only. A value shaped as a Cedar entity or extension (
__entity,__extn) is a deny before evaluation, with the argument's path in the receipt's errors, so an agent cannot satisfy a policy comparison by the shape of what it sends. - Receipts, #46: a stdio upstream is started with a minimal environment (the path, home, and temp directories, the locale, and what its config's
envnames), never the gateway's own, so the tokens other upstreams and the log are given do not reach a tool process. - Receipts, #41: a fact tool named in
precommitis refused at startup: a lookup runs before the policy decides and must be a read."*"covers the tools the agent calls, not the gateway's lookups. - Receipts, #45:
log --hash-only(AGENT_CUSTODY_HASH_ONLY=1, set by the compose file for the tenanted log) refuses{ leaf }at append and accepts only{ leafHash }, so a log run for other people never receives a receipt regardless of the client's configuration. - Receipts, #44: behind a trusted proxy, per-address limits key on the rightmost
X-Forwarded-Forentry, the one the proxy appended, never on one the client supplied. - Receipts and the browser verifier, #43: delegation windows are compared as instants in both verifiers; the package's grant schema accepts ISO offsets so both reach the same comparison. New vector
gateway-chain-offset-window-resigned, which both must refuse. - State, Python: unchanged; released in step.
0.6.1 — 2026-09-26
- Receipts: plans and monthly quotas. Every tenant is on a plan, free (ten thousand appends a calendar month), team (a million), or enterprise (no allowance); an append past the allowance is refused with 429, the numbers, and a
retry-afterreaching the start of next month, so the gateway behind it withholds pre-committed calls rather than acting without evidence. The plan is set on the admin page or withlog-admin tenant plan, audited, and reported with the quota on the tenant's usage route, the admin usage table, and the CSV. Existing tenants read as free. - Receipts: the tenant portal.
agent-custody portal(ROLE=portal, compose serviceportal, Caddy atPORTAL_HOST): a team registers with an email, a password, and a tenant id and gets the tenant and its first key shown once with the welcome sheet; the dashboard shows appends against the plan, the tree size and root, the latest checkpoint, the log's URLs, keys, and the audit rows; keys are minted and revoked there; the export command is on the page; and withSTRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET, andSTRIPE_PRICE_TEAMthe team plan is bought through Stripe Checkout, the signed webhook moving the plan. One inline page, no framework, strict content-security policy, signed-cookie sessions. - Deploy: the compose env file is optional, so the file validates without one.
- State, Python: unchanged; released in step.
0.6.0 — 2026-09-21
- Receipts: one gateway for many agents.
gateway --httpserves the gateway as an MCP server over Streamable HTTP; every connection presents its own grant (base64url inAuthorization: BearerorX-Agent-Custody-Grant) and gets a session under exactly that grant, sharing the upstreams, the policy, the key, and the log with the others and nothing else.createGatewayHostandhost.open(grant)are the library form;grantFilein the config is now optional. A stranger's, an expired, or a missing grant gets 403 with the reason. Tutorial 20. - Receipts: delegation chains for sub-agents. A grant that names the agent's own key (
grant --agent-key) lets the agent delegate a narrower grant to a sub-agent (delegate), parent embedded, up to three deep. The verifier, the browser verifier, and the gateway walk the chain back to a trusted principal key and refuse 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. New checkdelegation chain to the principal; vectorsgateway-chain-executedandgateway-chain-escalated-resigned. Existing grants are unchanged. - State: write-through adapters for Letta (archival passages, custody as tags), any LangGraph store, where LangMem keeps its memories (one item per fact under a namespace), and Cognee (its REST API, the data id found by listing the dataset, tested against a stand-in of its routes only). Closes #4.
- Repository: built under custody.
.claude/settings.jsonrecords every tool call an agent makes here as a receipt, hash-logged to our tenant on the hosted log; the site's custody page shows the live tenant and two verifiable receipts. The container image base moves to Node 24; Dependabot skips mermaid and Node majors. - Python: unchanged; released in step.
0.5.9 — 2026-09-10
- Receipts: the checkpoint publisher re-signs a quiet log's head every
--checkpoint-heartbeatseconds (default six hours;AGENT_CUSTODY_CHECKPOINT_HEARTBEATin the container) even when the tree has not grown. Before this a log with no appends for a day tripped the monitor's "checkpoint keeps up with the head" check, which requires a checkpoint at the head to be signed within twenty-four hours: a quiet log looked like a stalled publisher, and the hosted log's status went red on 2026-09-10 for that reason alone. The Postgres checkpoint store now updates the signature at an existing size when the root is unchanged and never when it differs. - Receipts: the witness names each alarm file uniquely; two refusals in the same millisecond no longer overwrite one another.
- State, Python: unchanged; released in step.
0.5.8 — 2026-09-09
- Receipts: an audit trail of administrative actions. Every tenant created or disabled and every token minted or revoked is recorded with who did it (the name entered at the admin page's prompt and the address,
bearerfor an API client, the user and host forlog-admin), what, which tenant, and the detail, never the token itself. The admin page shows it under Activity,GET /admin/auditandlog-admin auditlist it, a tenant reads their own rows atGET /t/<name>/auditwith their token, andlog-exportwrites them toaudit.json. - Receipts:
docs/threat-model.md, the attacker's view: every party who could make a receipt false, the move, what stops it, whether that is a property of the evidence or of the deployment, and what is not defended. - State, Python: unchanged; released in step.
0.5.7 — 2026-09-09
- Receipts: a tenant's export.
agent-custody log-export --log-url <url> --tenant <name> --token-env NAME --out <dir>fetches, with the tenant's own token, every leaf hash (GET /t/<name>/leaves, paged), the signed head, the published keys, the signed checkpoints, and their usage (GET /t/<name>/usage), checks that the head and every checkpoint verify against the keys and that the leaves hash to their roots, and writeslog.jsonlin the formatverify --logandaudit --logread. Both routes answer only to that tenant's token. The welcome sheet includes the command. - Receipts: every outbound request carries a timeout. The log client's is
log.timeoutMsin either config, default ten seconds, so a log that accepts connections and never answers cannot hold a pre-committed call; the key, witness, and consistency fetches time out at ten seconds. - Receipts: the signer publishes retired keys from
/data/keys/retired/*.pubin the container, so rotating the signing key is a copy and a restart with every old head still verifying. - Deploy:
RUNBOOK.md(upgrade and rollback, backups and the restore drill, token rotation, tenant export and offboarding, key rotation, leaked secrets, a compromised host, what each monitor failure means) andbackup-offsite.sh, which mirrors the nightly backups to an rclone remote named in.env.SECURITY.mdat the repository root is the disclosure policy. Dependabot watches npm, pip, GitHub Actions, and the image base. - State, Python: unchanged; released in step.
0.5.6 — 2026-09-09
- Receipts:
docs/compliance.md, the mapping from each artefact (receipt, authorization, denial, remote log, checkpoint, witness, ledger event, forget record, custody pack, explain output) to the SOC 2 criteria, ISO 27001 Annex A controls, EU AI Act articles, and UK GDPR articles it is evidence for, and what no artefact claims. - Receipts: Splunk export.
splunk: { url, tokenEnv, index?, source?, sourcetype?, host? }in either config sends every receipt as one event to a Splunk HTTP Event Collector, the token from the environment, with the receipt id, tool, agent, principal, status, decision, policy digest, and log position as plain fields; beside or instead ofotel, each exporter told independently, after the receipt and never on its path. Tutorial 19. - Receipts: the deployment guide states what a remote log costs per call, measured against the live log: about five milliseconds of server work plus the network round trip, twice for a pre-committed call.
- State:
agent-custody-memory review, the explain output as pages for the reviewer who will not open a terminal: an index of every receipt in a directory with when, tool, outcome, agent, principal, producer, and its verdict against the keys given, and one page per receipt with the ten answers, the verification report, and the bundle to download. Served on loopback with no login of its own, or written as static files with--outfor a case file. - State:
pgvectorStore(client, { embed, dimensions, table }), write-through to a pgvector table with the fact id as the row key and custody metadata beside the embedding; removal is verified by the same nearest-neighbour query recall would run. Tested against the pgvector image in Docker. The server now assigns the fact id before the stores are written, so every store sees the real id. - Python:
agent_custody.crewai.wrap_tools(client, tools), oneCustodyToolper CrewAI tool, same name, description, and schema, every run decided, executed, and recorded through the sidecar; a denied call never runs and the crew sees the denial as the tool's result. Extracrewai. The memory client now works with mcp 1.x and 2.x and reports a refused token as one plainMemoryError.
0.5.5 — 2026-09-08
- Receipts: monitoring.
agent-custody log-checkis the outside probe: it verifies a log's head against its published keys, that the latest checkpoint verifies and keeps up with the head, that the head extends the checkpoint, and, with a witness, that the witness has countersigned, keeps up, and has raised no alarm; it exits 1 on any failure. Themonitorworkflow runs it against log.agent-custody.dev every ten minutes from GitHub's machines and its badge is the status page.GET /healthis the liveness check. - Receipts: metering. Appends per tenant per month, leaves in total, and live tokens, on the admin page and as
/admin/usage.csv?month=YYYY-MM. - Python: unchanged; released in step.
0.5.4 — 2026-09-08
- Receipts: the witness.
agent-custody witnessruns on a machine the log's operator does not control, fetches each watched log's latest published checkpoint, verifies it against the log's published keys, proves with the log's own consistency proof that it extends the last head the witness signed, and countersigns it, DSSE allowing several signatures on one envelope; a rewritten history, a second history at the same size, or a checkpoint signed by an unpublished key gets an alarm file instead. It publishes its key at/.well-known/agent-custody-witness.json.audit --witness-urlor--witness-keyrequires the witness's signature on the newer head, and--older/--neweraccept checkpoint files from either host. Mirrored in the browser verifier. Phase 6 of issue #6: the code; running it needs a second operator. - Deploy:
deploy/witness/, a separate compose stack withROLE=witnessand its own host. - Python: unchanged; released in step.
0.5.3 — 2026-09-08
- Receipts:
log --trust-proxy. Behind a reverse proxy every request arrives from the proxy's address, so the per-address limits, the admin failure throttle above all, were shared by everyone and one client's wrong attempts could lock the page for all. With the flag, andTRUST_PROXY=1in the container, limits key on the firstX-Forwarded-Foraddress; off by default, since the header is otherwise the client's to forge. The compose file sets it because Caddy is the only way in. - Python: unchanged; released in step.
0.5.2 — 2026-09-08
- Receipts: the admin page itself is gated. Everything under
/admin, the page included, needs the admin token, presented by the browser's own prompt as HTTP Basic or by an API client as a bearer; wrong attempts from one address are throttled. The page stores nothing. In 0.5.1 the page shell was served without the token while every request it made required it. - Python: unchanged; released in step.
0.5.1 — 2026-09-08
- Receipts: the operator's admin page.
/adminon a Postgres-backed log server, behind--admin-token-env: tenants listed and created, a token minted and shown once beside the tenant's welcome sheet, tokens revoked, tenants disabled. One inline page, no outside requests.ADMIN_TOKENturns it on in the container. - Receipts: the combined checkpoint store reports the store furthest behind as latest, so a store that missed a write is caught up on the next publication; the image creates
/checkpointsowned by the log's user. - Deploy:
onboard-tenant.shfor the shell path; the early-access page says the log is running and taking tenants. - Tests: a thirty-second budget per test in every package; PGlite's engine load is paid in setup.
- Python: unchanged; released in step.
0.5.0 — 2026-09-08
- Receipts: the log over Postgres.
log --db-envkeeps leaves as hashes in one table keyed by tenant, one writer per tenant by advisory lock so a second instance is safe, tenants and sha256-hashed tokens in tables managed bylog-admin, rate limits per token with a body cap andretry-after, retries in the HTTP sink, andimportfor an existing file log. Phase 2 of issue #6. - Receipts: the signer, the key document, and checkpoints.
signerholds the log's key in its own process and the log signs through--signer-url; the log serves/.well-known/agent-custody-log.jsonandverify --log-urlandaudit --log-urlfetch and pin its keys by keyid; signed checkpoints per log are published to a directory and a table and listed at/checkpoints. Phase 3 of issue #6. - Deploy: Postgres and the signer are in the default compose profile, with a checkpoints volume served from a second Caddy host;
--profile filekeeps the single-file server. The image installspg. - Python: unchanged; released in step.
0.4.0 — 2026-09-08
- Receipts: a log for someone else.
"hashOnly": truein thelogconfig sends only the leaf hash, so a remote log commits to a receipt without ever holding it; the receipts stay with the issuer and the verifier is unchanged. The reference server accepts{leaf}or{leafHash}, serves several tenant logs at/t/<tenant>/from a--tenantsfile with their own tokens and ids, and writes--log-idinto every tree head.verify --log-idandaudit --log-idcheck that the tree heads name the expected log. Phase 1 of the hosted log, issue #6. - Deploy: the log server as a container,
ghcr.io/ch4r10t33r/agent-custody-log, built for amd64 and arm64 by a workflow on everyv*tag; a docker compose file for one VM with a Caddy TLS profile; Kubernetes manifests on the same contract;AGENT_CUSTODY_LOG_IDandAGENT_CUSTODY_LOG_TENANTSin it. - Site: the landing page is a quarter of its former length and says what the default install proves and what it does not; a trust-by-setup table; the hosted log stated as not yet built; certified forget defined by what it reaches; the gateway's bypasses listed.
- Python: unchanged; released in step.
0.3.0 — 2026-09-08
- Receipts: pre-commit authorization for consequential tools. Name them in
precommit(or*) and the gateway signs an authorization statement and appends it to the log before forwarding the call; if the log will not take it the call is withheld, nothing goes upstream, and the receipt recordsallowbesidewithheld. The receipt embeds the committed authorization with its inclusion proof, and the verifier adds five checks that it is the issuer's, names this call, is in the log, and precedes the receipt. Mirrored in the browser verifier; three new conformance vectors; tutorial 16. Evidence now precedes the side effect for the calls where that matters. - State:
agent-custody-memory explain. From a receipt id: who acted, who authorized it, what was allowed, what the agent saw, what it did, why, the evidence, whether it verifies, what depended on it, and what needs reversal. The first eight come from the receipt alone; the last two from the ledger, and without one they read "unknown".--out --signwrites the same as one signed action pack with every downstream receipt inside;--verifychecks it as a whole. - Receipts: the REST connector. An upstream may be a plain HTTP API described as tools,
{ "rest": { "baseUrl", "headerEnv", "tools": [...] } }, credentials read from the environment at startup. Scope, policy on the gateway's own lookups, pre-commit, and receipts apply unchanged, and a REST upstream sits beside MCP upstreams behind one grant. Tutorial 17. - Receipts: OpenTelemetry export.
otelin a gateway or SDK config sends one OTLP/HTTP span per receipt to the collector you already run, trace id equal to the receipt id, after the receipt and never on the evidence path. No OpenTelemetry SDK dependency. Tutorial 18. - Site: the landing page leads with proof of what an agent did, the six steps a call goes through, and the questions every receipt answers.
- Python: unchanged; released in step.
0.2.0 — 2026-09-07
- State, breaking: the ledger is asynchronous. Every method returns a promise,
sizeiscount(),close()andexport()return promises, andblastRadiusandbuildPackare awaited. Code written against 0.1.x must addawait; nothing else changes. - State: the ledger holds no events. Every question is a query to the store: everything about the facts matching a filter, everything that touched one fact, the spaces that exist. SQLite answers each from an index on fact, time, space, subject, predicate, and supersession, and fills those columns on a ledger written by an earlier version the first time it opens it. Closes #8.
- State: Postgres as a store, for a ledger shared by several servers in the database you already run.
--ledger postgres://…with thepgpackage installed, ornew PostgresStore(pool)in code. Forget is an update followed byVACUUM FULL, so the old row image does not stay in the table;vacuum: falseleaves that to your schedule. Tested against the real engine in-process through PGlite, including that a forgotten value is absent from the database files. - State:
Ledger.get,learnedBefore,spaces, andcompact; sweeps compact once at the end rather than per fact. - Site: the landing page says where the ledger lives.
0.1.9 — 2026-09-07
- State: the custody pack,
agent-custody-memory pack: one fact's history with the receipt behind each event, its holds, its blast radius with every downstream receipt, and its forget certificate with what the stores answered, as one signed artefact;pack --verifychecks the signature, the digest, every receipt inside against the gateway's keys, and that the forget receipt names the fact. - State: removal verification. After a retraction, forget, or sweep the server asks each store's own search whether the value still surfaces, with bounded retries, and records
verified,stillIndexed,unverified, orfailedper store in the result and so in the receipt. Mem0 and Zep verify through their search APIs. - State: the memory server warns at startup when no forget key is set, since a plain digest of a short value is guessable.
- Site: a deployment page with the architecture and measured sizing, and this changelog.
0.1.8 — 2026-09-07
- State: the eval harness as a CLI,
agent-custody-memory eval, with validated scenario files for a team's own incidents, the naive baseline scored beside the ledger, a non-zero exit on regression for cron, and a signed report a reviewer verifies witheval --verify. - State: a storage interface for the ledger. JSONL stays the default and the auditable file; a path ending in
.sqliteor.dbselects SQLite, with write-ahead logging, secure delete and a truncating checkpoint on forget so no erased value lingers, and a file several processes can open.exportwrites the JSONL of any ledger. - Receipts: provider-native deliveries. An upstream wrapping Stripe or GitHub attaches the signed webhook or delivery for a call; a verifier with the shared secret checks the HMAC, Stripe's timestamp against the receipt's, and the binding of the delivery to the result, and reports the execution as attested by shared secret.
verify --stripe-secret-env,--github-secret-env; three new conformance vectors; the browser verifier mirrors the check.
0.1.7 — 2026-09-07
- State: forget digests can be keyed under a secret held outside the ledger (
--forget-key-env) or omitted (keepDigest: false), so an erased value cannot be guessed back from the file. The event records which. - State: retention windows per space in the server (
--retention 'org=P365D,team:*=P90D'); a sweep with no cutoff uses them;sweep --via gateway.jsonruns retention as a receipted call by the principal in that gateway's grant, for any timer. - Receipts:
prunereplaces log leaves older than a cutoff with their hashes and removes their bundles; every proof still verifies. - Receipts: the Merkle log caches complete subtrees, so appends and proofs are logarithmic; receipt cost is flat at 0.15 ms regardless of log size.
- A test loads every receipts module under plain Node, after a construct plain Node rejects broke process-spawning tests for one commit.
0.1.6 — 2026-09-07
- Receipts: several upstreams behind one gateway and one grant, each tool owned by exactly one; the receipt names which served the call.
- State: value-level quarantine. A write may cite a fact the gateway fetched itself; the value must match and the fact is written as
verified, the provenance level aboveattested, or the write is refused.requireVerifiedon reads. - State: retention sweeps and legal holds, as events with actor, reason, and receipt; a held fact cannot be forgotten or swept until released.
- Python: the memory tools over MCP against the shared memory server. Python 3.10 or later.
0.1.5 — 2026-09-07
- Receipts: consumed facts. An upstream declares the facts it served; every later gateway receipt in the session carries them, observed.
- State: blast radius: from a fact to every later call and derived belief, transitively, with retraction status.
agent-custody-memory blast. - State: policy over provenance: the gateway looks up the fact a write supersedes or a retraction targets through
memory.get; optional fact lookups in the gateway. - State: certified forget: the value erased from the ledger and every store, the gateway's receipt as the certificate.
- State: the memory server over HTTP with bearer auth; the gateway reaches any running MCP server by URL.
- Receipts: attested execution: an upstream signs its result for the receipt; the memory server and the demo upstream sign with
--key.
0.1.4 — 2026-09-07
- State: quarantine. Facts carry
claimedorattestedprovenance; claimed facts are hidden from reads until a gateway-onlymemory.confirmlifts them. - State: the memory-mutation eval harness, with the ledger and a naive overwrite store as reference points.
- State: write-through adapters for Mem0 and Zep, tested against the real client packages offline; custody metadata travels with the write, retractions reach the store.
0.1.3 — 2026-09-07
- Receipts: the log sink: append to a remote log whose key signs the tree heads; the reference log server with bearer auth; consistency proofs and the
auditcommand; the sidecar (serve) for agents in other languages. - State: the memory server: the ledger as MCP tools behind the gateway, with the source receipt id and the attested actor supplied by the gateway.
- Receipts: conformance vectors published with the spec.
0.1.2 — 2026-09-06
- Receipts: the sidecar, required by the Python client.
- Python:
agent-custodyon PyPI: a standard-library client of the sidecar with adapters for LangChain, the OpenAI Agents SDK, and the Claude Agent SDK. Go, Java, and Rust clients as examples.
0.1.1 — 2026-09-06
- The root walkthrough, the cross-package example, and the first end-to-end use of both packages from a fresh install.
0.1.0 — 2026-09-06
- First publish:
@agent-custody/receipts(gateway, SDK, log, verifier) and@agent-custody/state(the bitemporal fact ledger).