@agent-custody/state 0.1.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,11 +15,12 @@ Published on npm as [`@agent-custody/state`](https://www.npmjs.com/package/@agen
15
15
  ```ts
16
16
  import { Ledger } from "@agent-custody/state";
17
17
 
18
+ // a JSONL file, a .sqlite path, or "postgres://…" with the pg package installed; the ledger is asynchronous
18
19
  const ledger = new Ledger("./state/ledger.jsonl");
19
- const a = ledger.assert({ subject: "acct:42", predicate: "plan", value: "pro", space: "org", actor: "agent:support", source: { receiptId } });
20
- ledger.assert({ subject: "acct:42", predicate: "plan", value: "enterprise", space: "org", actor: "agent:sales", supersedes: a.fact.factId });
21
- ledger.retract({ factId: a.fact.factId, actor: "user:admin", reason: "poisoned by a tool result" });
22
- ledger.asOf({ validAt: "2026-09-01T00:00:00Z", txAt: "2026-09-01T00:00:00Z" });
20
+ const a = await ledger.assert({ subject: "acct:42", predicate: "plan", value: "pro", space: "org", actor: "agent:support", source: { receiptId } });
21
+ await ledger.assert({ subject: "acct:42", predicate: "plan", value: "enterprise", space: "org", actor: "agent:sales", supersedes: a.fact.factId });
22
+ await ledger.retract({ factId: a.fact.factId, actor: "user:admin", reason: "poisoned by a tool result" });
23
+ await ledger.asOf({ validAt: "2026-09-01T00:00:00Z", txAt: "2026-09-01T00:00:00Z" });
23
24
  ```
24
25
 
25
26
  Six runnable examples, all executed by the test suite. [06-actions-on-beliefs.ts](examples/06-actions-on-beliefs.ts) puts memory and a payments API behind one gateway and finds the refund in a belief's blast radius. [05-blast-radius.ts](examples/05-blast-radius.ts) walks from a retracted belief to everything that relied on it. [04-evals.ts](examples/04-evals.ts) scores the ledger and a naive store on the same memory incidents. [03-memory-behind-the-gateway.ts](examples/03-memory-behind-the-gateway.ts) runs the memory server as the gateway's upstream. [01-ledger.ts](examples/01-ledger.ts) walks through a wrong write and its undo. [02-receipt-to-belief.ts](examples/02-receipt-to-belief.ts) runs the whole loop with the receipts package: a tool call gets a signed receipt, the receipt is verified, the belief taken from it is recorded citing the receipt, and later retracted. Run them with `node examples/<file>` from this directory, after `bun run build` at the repository root.
@@ -81,12 +82,12 @@ The lookup for writes is optional, so a write that supersedes nothing needs no l
81
82
 
82
83
  ## Certified forget
83
84
 
85
+ "Certified" means the receipt certifies what was done, not that the value exists nowhere. What forget reaches is the ledger and the stores with adapters, and it reports what each of them answered. It does not reach caches, a model's context window, application logs, replicas, backups, warehouses fed by export, or any store without an adapter. On Postgres the erased row image stays in the table until the store's `VACUUM FULL` runs, which it does after each forget unless told not to ([the ledger](#the-ledger)). Say all of that plainly to whoever is relying on it, and read the per-store status in the result before calling anything gone.
86
+
84
87
  A deletion demand is different from a correction. Retract keeps the record; forget erases the value. `memory.forget` removes the fact's value from the ledger file itself, stops believing the fact, and removes it from every store behind the server. What it keeps in place of the value is a choice, recorded on the event as `digestKind`: a plain `sha256` of the value, which lets the ledger prove what it erased but is guessable for short values such as an email by anyone holding the file; an `hmac-sha256` under a forget key the server holds outside the file (`serve --forget-key-env NAME`), which proves the same to anyone shown the key and nothing to anyone else; or `none`, with `keepDigest: false`, for the case where counsel wants nothing derived from the value retained. Use the keyed form unless you have a reason not to. The result says exactly what happened: erased from the ledger, removed from which stores, still held by which, with the digest, the actor, and the reason.
85
88
 
86
89
  **Retention and legal hold.** `memory.sweep` forgets every fact the ledger learned of before an instant, in one space or all, and removes each from every store; it is retention as a receipted call, with the receipt as the record of what was erased. Retention windows live in the server: `serve --retention 'org=P365D,team:*=P90D,user:*=P30D'`, ISO 8601 durations by space pattern, and a sweep with no `before` uses them per space. To run it on a schedule without a daemon, `agent-custody-memory sweep --via retention-gateway.json --reason "quarterly retention"` spawns that gateway and calls `memory.sweep` through it, so the sweep runs as the principal named in the gateway's grant, a retention job with the single scope `memory.sweep`, and its receipt records when retention ran, by whom, what it erased, and what a hold kept. Any cron, systemd timer, or CI schedule drives that one command. `memory.hold` puts a legal hold on a fact: while it stands, neither a deletion request nor a sweep can forget it, and `memory.release` lifts it. Holds and releases are events with actor, reason, and receipt, so the history of a fact shows the hold as plainly as the write. `agent-custody-memory sweep --ledger ... --before ... --reason ...` runs retention on the ledger file alone, for ledgers with no stores behind them.
87
90
 
88
- What forget reaches is the ledger and the stores with adapters. It does not reach caches, a model's context window, application logs, or any store without an adapter. Say that plainly to whoever is relying on it.
89
-
90
91
  **Removal is verified, not assumed.** A delete by id and a search index catching up are different moments. After every retraction, forget, or sweep, the server asks each store's own search whether the fact still surfaces, a few times with backoff, and records the outcome per store in the result and so in the receipt: `verified` (the store's search no longer finds it), `stillIndexed` (it still does, after the retries), `unverified` (the store cannot be asked, or the adapter has no search), or `failed` (the removal itself failed). Mem0 and Zep both verify through their search APIs; the tests drive the real clients against indexes that lag by a configurable number of searches. A certificate that says `stillIndexed` is an honest certificate; run forget again later, or read it as the store's problem to fix.
91
92
 
92
93
  **The pack.** `agent-custody-memory pack --ledger ... --receipts ... --fact <id> --out pack.json --sign keys/pack.key` gathers everything about one fact into one signed artefact: its history with the receipt that produced each event, its holds and releases, its blast radius with every downstream receipt, and, if it was forgotten, the forget certificate with what each store answered. `pack --verify pack.json --key keys/pack.pub --issuer-key keys/gateway.pub --principal-key keys/principal.pub` checks the pack's signature and digest, every receipt inside it against the gateway's keys, that every event's receipt is present, and that the forget receipt names this fact and records the erasure. Touch one receipt inside and the whole pack fails. It is the artefact counsel attaches to a ticket; the reviewer needs the two public keys and nothing from you.
@@ -118,6 +119,29 @@ With the gateway fronting several upstreams, memory and the tools the agent acts
118
119
 
119
120
  It is an upper bound by design: a call made after the agent had seen the fact is in the radius whether or not the agent used it, because no receipt can prove what a model attended to. What it never misses is the thing that matters, a downstream action or belief that did depend on the fact. [examples/05-blast-radius.ts](examples/05-blast-radius.ts) runs the whole loop.
120
121
 
122
+ ## Explain one action
123
+
124
+ The question a security owner asks first is not about a fact but about an action: *what is refund 183722, and can I trust the answer?* `agent-custody-memory explain --receipts receipts --receipt <id> --ledger ledger.sqlite --issuer-key keys/gateway.pub --principal-key keys/principal.pub` answers it in the order it is asked:
125
+
126
+ ```
127
+ WHO support-agent (attested, named in a signed grant)
128
+ WHO AUTHORIZED IT user_456, grant signed by key bfa1c0d2e3f4, valid 2026-09-08T08:00:00.000Z to 2026-09-08T16:00:00.000Z
129
+ WHAT WAS ALLOWED tools customer.lookup, stripe.refund
130
+ policy 9c1d2e3f4a5b decided allow
131
+ WHAT THE AGENT SAW customer = {"verified":true,"plan":"pro"} (fetched by the gateway via customer.lookup)
132
+ 1 belief(s) shown before this call:
133
+ acct:42 plan = "enterprise" [0b83d822]
134
+ WHAT IT DID stripe.refund {"customer_id":"cust_123","amount":845000} -> executed
135
+ WHY the policy permitted it
136
+ WHAT EVIDENCE receipt 7f2e…, leaf 41 of a log whose head is signed by 3a9b0c1d2e3f
137
+ authorization committed as leaf 40, before the call was forwarded
138
+ CAN I VERIFY IT VERIFIED, 23 checks
139
+ DID ANYTHING DEPEND ON THIS 1 belief(s) written in this call, 3 later call(s) made after seeing them, 2 belief(s) derived
140
+ WHAT NEEDS REVERSAL 2 belief(s) still believed, and 3 later call(s) to review
141
+ ```
142
+
143
+ The first eight lines come from the receipt alone and work without a ledger; the last two are answered from the ledger, and without one they say `unknown without a ledger` rather than pretending nothing depended on the call. `--out action.json --sign keys/pack.key` writes the same answers as one signed action pack with the receipt and every downstream receipt inside it, and `explain --verify action.json --key keys/pack.pub --issuer-key ... --principal-key ...` checks the pack's signature and digest, the receipt, every downstream receipt, and that the written beliefs cite this receipt. Touch one receipt inside and the pack fails. The custody pack below is the same artefact seen from a fact instead of an action.
144
+
121
145
  ## Write-through to the stores you already use
122
146
 
123
147
  The ledger is not a retrieval store, and it does not try to be. `src/stores.ts` puts it under the ones teams already run: a fact written through the memory server also lands in every configured store, with its custody metadata (fact id, space, actor, provenance, receipt id), the store's own id is recorded on the fact, and a retraction reaches the store by that id. Certified forget will be built on this: a deletion is only real once it has reached the stores that serve recall.
@@ -157,9 +181,13 @@ A scenario file is `{ "version": "0.1", "scenarios": [{ "name", "ops": [...] }]
157
181
 
158
182
  ## The ledger
159
183
 
160
- `src/ledger.ts` keeps an append-only log of events, in memory for its queries and in one of two stores for durability. **JSONL** is the default: one event per line, readable by anyone, the file you copy for an audit. **SQLite** is for durability and shared use, chosen by giving the ledger a path ending in `.sqlite` or `.db`: transactional writes with write-ahead logging, an in-place forget that overwrites the erased value (secure delete, then a truncating checkpoint so nothing lingers in the write-ahead log), indexes by fact and by time, and a file more than one process can open. Node ships the SQLite module, so there is no native dependency; on Node 22 it prints an experimental warning once. `agent-custody-memory export --ledger ledger.sqlite --out ledger.jsonl` writes the auditable JSONL from any store, so the copy-the-file audit path survives the choice. The whole ledger test suite runs against both stores.
184
+ `src/ledger.ts` keeps an append-only log of events. It holds none of them itself: every question is a query to a store, so the store decides how large a ledger can be and who shares it. A store answers four questions, everything in order (export, audits), everything about the facts matching a filter (a bitemporal read), everything that touched one fact (its history and its checks), and which spaces exist (retention), and it does three things: append, replace one assert in place (forget), and compact after erasures. Three stores ship, and the whole ledger suite runs against each of them, so they answer identically.
185
+
186
+ **JSONL** is the default: one event per line, readable by anyone, the file you copy for an audit. It answers from memory, which is fine to tens of thousands of events. **SQLite**, chosen by a path ending in `.sqlite` or `.db`, is for durability on one machine: transactional writes with write-ahead logging, an in-place forget that overwrites the erased value (secure delete, then a truncating checkpoint so nothing lingers in the write-ahead log), and every query answered by an index on fact, time, space, subject, predicate, and supersession. Node ships the SQLite module, so there is no native dependency; a ledger written by an earlier version gets the index columns filled from its JSON the first time it is opened. Measured on a million-event ledger: open in 0.2 s, a subject read in about 1 ms, a fact's history in well under a millisecond, the spaces for a sweep in 2 ms. **Postgres**, chosen by a `postgres://` URL, is for a shared ledger: several memory servers on one table, in the database your security team has already approved, the same queries pushed down as SQL. It needs the `pg` package installed beside this one; `?table=custody.events` names the table and `?vacuum=false` skips the vacuum described below. In code, pass a `PostgresStore` around your own `pg` Pool, with whatever TLS and credentials you already use. The adapter is tested against the real Postgres engine in-process through PGlite, including that a forgotten value is absent from the database files, and checked by hand against Postgres 16.
187
+
188
+ Forget on Postgres is an `UPDATE`, and MVCC keeps the old row image in the table until vacuum. So after a forget or a sweep the store runs `VACUUM FULL` on the table, which rewrites it without the old image. That takes an exclusive lock for the rewrite; a very large ledger may set `vacuum: false` and run its own schedule, and the forget certificate then rests on that schedule. The write-ahead log, replicas, and backups keep their own copies for as long as their retention says; that is true of every database, and the honest reading of a forget certificate is that the live ledger no longer holds the value.
161
189
 
162
- Choose JSONL for one server process and for anything an auditor should be able to read with `cat`. Choose SQLite when the ledger is shared over HTTP by several gateways, when a crash between two writes must not cost you an event, or when a deletion demand must leave no trace of the value in the file. Query pushdown to SQLite's indexes is not done yet: both stores load every event into memory, which is fine to tens of thousands of facts. [Issue #8](https://github.com/ch4r10t33r/agent-custody/issues/8) tracks indexed queries for larger ledgers.
190
+ Choose JSONL for one server process and for anything an auditor should read with `cat`. Choose SQLite when one machine serves the ledger over HTTP, when a crash between two writes must not cost you an event, or when a deletion demand must leave no trace of the value in the file. Choose Postgres when more than one server must share the ledger, or when the ledger belongs in the database you already operate. Warehouses are not stores: Snowflake and Databricks keep deleted rows for days by default and are built for scans, not one small write per agent call. Feed them with `agent-custody-memory export --ledger postgres://… --out ledger.jsonl`, which writes the auditable JSONL from any store, and query the ledger where your compliance team already works.
163
191
 
164
192
  The log holds these kinds of event.
165
193
 
@@ -181,11 +209,12 @@ The ledger refuses to supersede a fact that is unknown, already superseded, or r
181
209
 
182
210
  ```
183
211
  src/ledger.ts the fact record, the event kinds, as-of queries, supersession, retraction, forget, holds, sweeps
184
- src/storage.ts the event stores: JSONL (default, auditable) and SQLite (durable, shared), chosen by file extension
212
+ src/storage.ts the store interface and its three stores: JSONL (default, auditable), SQLite (indexed, one machine), Postgres (shared); chosen by path or URL
185
213
  src/server.ts the ledger as MCP tools; source and actor taken from the gateway's _meta
186
214
  src/http.ts the memory server over Streamable HTTP with bearer auth, for a shared ledger
215
+ src/explain.ts one action explained: the ten answers from a receipt and the ledger, and the signed action pack
187
216
  src/pack.ts the custody pack: build, sign, verify, format
188
- src/cli.ts agent-custody-memory serve (stdio or --http, with --retention and --forget-key-env), sweep (ledger-only or --via a gateway), eval, pack, export, blast
217
+ src/cli.ts agent-custody-memory serve (stdio or --http, with --retention and --forget-key-env), sweep (ledger-only or --via a gateway), eval, explain, pack, export, blast
189
218
  src/blast.ts blast radius: from receipts' consumed facts and the ledger's source receipts, forward
190
219
  src/stores.ts write-through adapters: Mem0 and Zep, and the Store interface for others
191
220
  src/evals.ts the memory-mutation harness: scenarios, scoring, report
@@ -201,10 +230,11 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
201
230
 
202
231
  **Done**
203
232
 
204
- - Bitemporal fact ledger with supersession, retraction, as-of and history queries, persisted as JSONL or SQLite behind one store interface, with export back to JSONL.
233
+ - Bitemporal fact ledger with supersession, retraction, as-of and history queries, persisted as JSONL, SQLite, or Postgres behind one store interface that answers every query from an index, with export back to JSONL.
205
234
  - The memory server: the ledger as MCP tools behind the receipts gateway, with the source receipt id and the attested actor supplied by the gateway, policy over spaces, and a denial receipt for every refused write.
206
235
  - Forget digests are keyed under a server-held secret, or absent on request, so an erased value cannot be guessed back from the file.
207
236
  - Retention windows per space in the server, sweeps that default to them, and a `sweep --via` trigger that runs retention through a gateway as a named principal, on any timer.
237
+ - Explain one action: from a receipt id, who, 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 same as a signed action pack that carries every downstream receipt.
208
238
  - The custody pack: a fact's history with receipts, holds, blast radius, and forget certificate as one signed artefact, verified as a whole.
209
239
  - Removal verification: after a retraction, forget, or sweep the server asks each store's search whether the value still surfaces and records verified, stillIndexed, unverified, or failed per store, in the receipt.
210
240
  - Retention and legal hold: a receipted sweep forgets what was learned before an instant and reaches the stores; a hold refuses forget and sweep until released, as events on the fact's history.
@@ -221,7 +251,6 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
221
251
 
222
252
  **Next, in the order it pays off**
223
253
 
224
- 1. Indexed queries on the SQLite store, so a shared ledger with millions of events answers reads without loading them all. [Issue #8](https://github.com/ch4r10t33r/agent-custody/issues/8).
254
+ 1. The hosted plane, behind early access: tenanted log, then memory, then reports and a control plane, with SSO, SCIM, residency, and SIEM export. [Issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
225
255
  2. Write-through adapters for Letta, LangMem, and Cognee, one per user who asks. [Issue #4](https://github.com/ch4r10t33r/agent-custody/issues/4).
226
- 3. The hosted plane, behind early access: tenanted log, then memory, then reports and a control plane, with SSO, SCIM, residency, and SIEM export. [Issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
227
256
 
package/dist/blast.d.ts CHANGED
@@ -21,5 +21,5 @@ export interface BlastRadius {
21
21
  /** derived facts that are still believed; the ones a cleanup has to decide about */
22
22
  stillBelieved: Fact[];
23
23
  }
24
- export declare function blastRadius(ledger: Ledger, receipts: ReceiptSummary[], factId: string): BlastRadius;
24
+ export declare function blastRadius(ledger: Ledger, receipts: ReceiptSummary[], factId: string): Promise<BlastRadius>;
25
25
  export declare function formatBlastRadius(b: BlastRadius, factId: string): string;
package/dist/blast.js CHANGED
@@ -20,10 +20,10 @@ export function loadReceipts(dir) {
20
20
  }
21
21
  return out.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
22
22
  }
23
- export function blastRadius(ledger, receipts, factId) {
24
- const all = ledger.facts();
23
+ export async function blastRadius(ledger, receipts, factId) {
24
+ const all = await ledger.facts();
25
25
  const byId = new Map(all.map((f) => [f.factId, f]));
26
- const believed = new Set(ledger.asOf().map((f) => f.factId));
26
+ const believed = new Set((await ledger.asOf()).map((f) => f.factId));
27
27
  const seen = new Set([factId]);
28
28
  const hit = new Map();
29
29
  const derived = new Map();
@@ -44,7 +44,7 @@ export function blastRadius(ledger, receipts, factId) {
44
44
  grew = true;
45
45
  }
46
46
  }
47
- const retraction = ledger.history(factId).find((e) => e.kind === "retract") ?? null;
47
+ const retraction = (await ledger.history(factId)).find((e) => e.kind === "retract") ?? null;
48
48
  const derivedFacts = [...derived.values()];
49
49
  return { fact: byId.get(factId) ?? null, receipts: [...hit.values()].sort((a, b) => a.timestamp.localeCompare(b.timestamp)), derivedFacts, retraction, stillBelieved: derivedFacts.filter((f) => believed.has(f.factId)) };
50
50
  }
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { Ledger } from "./ledger.js";
4
4
  import { createMemoryServer, serveStdio } from "./server.js";
5
5
  import { blastRadius, formatBlastRadius, loadReceipts } from "./blast.js";
6
6
  import { serveMemoryHttp } from "./http.js";
7
- import { loadPrivateKey, loadPublicKey } from "@agent-custody/receipts";
7
+ import { loadPrivateKey, loadPublicKey, verifyBundle } from "@agent-custody/receipts";
8
8
  import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
@@ -12,6 +12,7 @@ import { formatReport, runAll, SCENARIOS } from "./evals.js";
12
12
  import { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.js";
13
13
  import { loadScenarios, signReport, verifyReport } from "./evals-file.js";
14
14
  import { buildPack, formatPack, signPack, verifyPack } from "./pack.js";
15
+ import { buildActionPack, formatExplain, signActionPack, verifyActionPack } from "./explain.js";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
18
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -37,13 +38,15 @@ function parseRetention(spec) {
37
38
  }
38
39
  const USAGE = `agent-custody-memory <command>
39
40
 
40
- serve --ledger <ledger.jsonl|ledger.sqlite> [--allow-direct] [--key <memory.key>] [--forget-key-env NAME] [--retention 'org=P365D,team:*=P90D']
41
+ serve --ledger <ledger.jsonl|ledger.sqlite|postgres://…> [--allow-direct] [--key <memory.key>] [--forget-key-env NAME] [--retention 'org=P365D,team:*=P90D']
41
42
  --forget-key-env names a secret kept outside the ledger; forgotten values then leave an HMAC, not a guessable hash.
42
43
  the memory server over stdio; run it as the receipts gateway's upstream.
43
44
  --key signs every result for its receipt, so executions verify as attested by this server.
44
45
  By default it refuses calls that did not come through the gateway.
45
46
  serve --ledger <ledger.jsonl> --http [--port 8790] [--host 127.0.0.1] [--token-env NAME] [--allow-direct]
46
- the same server shared over HTTP: several gateways, one ledger
47
+ the same server shared over HTTP: several gateways, one ledger.
48
+ A postgres:// ledger (needs the pg package; ?table=custody.events names the table)
49
+ is shared by every server pointed at it.
47
50
  sweep --via <gateway.json> --reason <text> [--before <ISO instant>] [--space <space>] [--no-digest]
48
51
  retention as a receipted call: runs memory.sweep through that gateway, as the principal in its grant.
49
52
  Without --before, the memory server's --retention windows decide. Put this on a timer.
@@ -58,7 +61,14 @@ const USAGE = `agent-custody-memory <command>
58
61
  holds, the forget certificate and what the stores answered. For counsel and auditors.
59
62
  pack --verify <pack.json> --key <pub> [--issuer-key <pub>] [--principal-key <pub>]
60
63
  checks the pack's signature and every receipt inside it
61
- export --ledger <ledger.sqlite> --out <ledger.jsonl> the auditable JSONL of any ledger, one event per line
64
+ explain --receipts <dir> --receipt <receiptId> [--ledger <ledger>] [--issuer-key <pub>] [--principal-key <pub>] [--log-key <pub>] [--json]
65
+ one action, answered: who, who authorized it, what was allowed, what the agent saw, what it did,
66
+ why, the evidence, whether it verifies, what depended on it, what needs reversal.
67
+ With a ledger the last two are answered from the beliefs; without, they say so.
68
+ explain ... --out <action.json> --sign <key> the same as one signed action pack, every downstream receipt inside
69
+ explain --verify <action.json> --key <pub> [--issuer-key <pub>] [--principal-key <pub>] [--log-key <pub>]
70
+ checks the pack, the receipt inside it, and every downstream receipt
71
+ export --ledger <ledger.sqlite|postgres://…> --out <ledger.jsonl> the auditable JSONL of any ledger, one event per line; also the feed for a warehouse
62
72
  blast --ledger <ledger.jsonl> --receipts <dir> --fact <factId> [--json]
63
73
  everything that relied on a fact: later calls, derived beliefs, and whether it was retracted
64
74
  `;
@@ -80,14 +90,14 @@ async function main(argv) {
80
90
  if (values["token-env"] && !token)
81
91
  throw new Error(`serve: environment variable ${values["token-env"]} is not set`);
82
92
  const running = await serveMemoryHttp(ledger, { port: Number(values.port), host: values.host, ...common, ...(token ? { tokens: [token] } : {}) });
83
- console.error(`agent-custody-memory: ${running.url} ledger=${values.ledger} events=${ledger.size} ${token ? "bearer token required" : "open"} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
93
+ console.error(`agent-custody-memory: ${running.url} ledger=${values.ledger} events=${await ledger.count()} ${token ? "bearer token required" : "open"} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
84
94
  if (digestWarning)
85
95
  console.error(digestWarning);
86
96
  await new Promise((resolve) => process.once("SIGINT", resolve));
87
97
  await running.close();
88
98
  return 0;
89
99
  }
90
- console.error(`agent-custody-memory: ledger=${values.ledger} events=${ledger.size} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
100
+ console.error(`agent-custody-memory: ledger=${values.ledger} events=${await ledger.count()} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
91
101
  if (digestWarning)
92
102
  console.error(digestWarning);
93
103
  await serveStdio(createMemoryServer(ledger, common));
@@ -115,10 +125,12 @@ async function main(argv) {
115
125
  }
116
126
  if (!values.ledger || !values.before || !values.reason)
117
127
  throw new Error("sweep needs --ledger, --before, and --reason, or --via <gateway.json> --reason");
118
- const r = new Ledger(values.ledger, forgetKeyFrom(values["forget-key-env"])).sweep({ before: new Date(values.before).toISOString(), ...(values.space ? { space: values.space } : {}), actor: values.actor, reason: values.reason, ...(values["no-digest"] ? { keepDigest: false } : {}) });
128
+ const sweeper = new Ledger(values.ledger, forgetKeyFrom(values["forget-key-env"]));
129
+ const r = await sweeper.sweep({ before: new Date(values.before).toISOString(), ...(values.space ? { space: values.space } : {}), actor: values.actor, reason: values.reason, ...(values["no-digest"] ? { keepDigest: false } : {}) });
119
130
  console.log(`forgot ${r.forgotten.length} fact(s); ${r.held.length} on hold, kept`);
120
131
  for (const f of r.forgotten)
121
132
  console.log(` ${f.factId} ${f.digestKind}${f.valueDigest ? ` ${f.valueDigest.slice(0, 12)}` : ""}`);
133
+ await sweeper.close();
122
134
  return 0;
123
135
  }
124
136
  case "eval": {
@@ -177,27 +189,68 @@ async function main(argv) {
177
189
  }
178
190
  if (!values.ledger || !values.receipts || !values.fact || !values.out || !values.sign)
179
191
  throw new Error("pack needs --ledger, --receipts, --fact, --out, and --sign");
180
- const pack = buildPack(new Ledger(values.ledger), values.receipts, values.fact);
192
+ const packLedger = new Ledger(values.ledger);
193
+ const pack = await buildPack(packLedger, values.receipts, values.fact);
194
+ await packLedger.close();
181
195
  writeFileSync(values.out, JSON.stringify(signPack(pack, loadPrivateKey(values.sign)), null, 2));
182
196
  console.log(formatPack(pack));
183
197
  console.error(`signed pack written to ${values.out}`);
184
198
  return pack.missingReceipts.length ? 1 : 0;
185
199
  }
200
+ case "explain": {
201
+ const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, receipts: { type: "string" }, receipt: { type: "string" }, out: { type: "string" }, sign: { type: "string" }, verify: { type: "string" }, key: { type: "string", multiple: true }, "issuer-key": { type: "string", multiple: true }, "principal-key": { type: "string", multiple: true }, "log-key": { type: "string", multiple: true }, json: { type: "boolean", default: false } } });
202
+ const receiptKeys = values["issuer-key"]?.length ? { issuerKeys: values["issuer-key"].map(loadPublicKey), principalKeys: (values["principal-key"] ?? []).map(loadPublicKey), ...(values["log-key"]?.length ? { logKeys: values["log-key"].map(loadPublicKey) } : {}) } : undefined;
203
+ if (values.verify) {
204
+ if (!values.key?.length)
205
+ throw new Error("explain --verify needs --key <pub>");
206
+ const r = verifyActionPack(JSON.parse(readFileSync(values.verify, "utf8")), values.key.map(loadPublicKey), receiptKeys);
207
+ if (values.json)
208
+ console.log(JSON.stringify(r, null, 2));
209
+ else {
210
+ for (const c of r.checks)
211
+ console.log(`${c.ok ? "PASS" : "FAIL"} ${c.name}${c.detail ? ` (${c.detail})` : ""}`);
212
+ console.log(`\nRESULT: ${r.ok ? "VERIFIED" : "NOT VERIFIED"}`);
213
+ if (r.pack)
214
+ console.log("\n" + formatExplain(r.pack, r.receipt, true));
215
+ }
216
+ return r.ok ? 0 : 1;
217
+ }
218
+ if (!values.receipts || !values.receipt)
219
+ throw new Error("explain needs --receipts and --receipt, or --verify");
220
+ const ledger = values.ledger ? new Ledger(values.ledger) : undefined;
221
+ const pack = await buildActionPack(values.receipts, values.receipt, ledger);
222
+ await ledger?.close();
223
+ const verification = receiptKeys ? verifyBundle(pack.receipt, receiptKeys) : null;
224
+ if (values.out) {
225
+ if (!values.sign)
226
+ throw new Error("explain --out needs --sign <key>");
227
+ writeFileSync(values.out, JSON.stringify(signActionPack(pack, loadPrivateKey(values.sign)), null, 2));
228
+ console.error(`signed action pack written to ${values.out}`);
229
+ }
230
+ if (values.json)
231
+ console.log(JSON.stringify({ pack, verification }, null, 2));
232
+ else
233
+ console.log(formatExplain(pack, verification, !!ledger));
234
+ return verification && !verification.ok ? 1 : pack.missingReceipts.length ? 1 : 0;
235
+ }
186
236
  case "export": {
187
237
  const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, out: { type: "string" } } });
188
238
  if (!values.ledger || !values.out)
189
239
  throw new Error("export needs --ledger and --out");
190
240
  const l = new Ledger(values.ledger);
191
- writeFileSync(values.out, l.export().map((e) => JSON.stringify(e)).join("\n") + "\n");
192
- console.log(`exported ${l.size} event(s) from ${l.location} to ${values.out}`);
193
- l.close();
241
+ const events = await l.export();
242
+ writeFileSync(values.out, events.map((e) => JSON.stringify(e)).join("\n") + "\n");
243
+ console.log(`exported ${events.length} event(s) from ${l.location} to ${values.out}`);
244
+ await l.close();
194
245
  return 0;
195
246
  }
196
247
  case "blast": {
197
248
  const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, receipts: { type: "string" }, fact: { type: "string" }, json: { type: "boolean", default: false } } });
198
249
  if (!values.ledger || !values.receipts || !values.fact)
199
250
  throw new Error("blast needs --ledger, --receipts, and --fact");
200
- const b = blastRadius(new Ledger(values.ledger), loadReceipts(values.receipts), values.fact);
251
+ const blastLedger = new Ledger(values.ledger);
252
+ const b = await blastRadius(blastLedger, loadReceipts(values.receipts), values.fact);
253
+ await blastLedger.close();
201
254
  console.log(values.json ? JSON.stringify(b, null, 2) : formatBlastRadius(b, values.fact));
202
255
  return b.fact ? 0 : 1;
203
256
  }
@@ -1,9 +1,9 @@
1
1
  export function ledgerUnderTest(ledger) {
2
2
  return {
3
- write: (i) => ({ id: ledger.assert({ ...i, provenance: "attested" }).fact.factId }),
4
- read: (q) => ledger.asOf({ ...q, include: "attested" }).map(({ subject, predicate, value }) => ({ subject, predicate, value })),
5
- retract: (i) => {
6
- ledger.retract({ factId: i.id, actor: i.actor, reason: i.reason });
3
+ write: async (i) => ({ id: (await ledger.assert({ ...i, provenance: "attested" })).fact.factId }),
4
+ read: async (q) => (await ledger.asOf({ ...q, include: "attested" })).map(({ subject, predicate, value }) => ({ subject, predicate, value })),
5
+ retract: async (i) => {
6
+ await ledger.retract({ factId: i.id, actor: i.actor, reason: i.reason });
7
7
  },
8
8
  };
9
9
  }
@@ -0,0 +1,53 @@
1
+ import { type Envelope, type KeyPair, type PublicKeyRef, type ReceiptBundle, type ReceiptStatement, type VerifyOptions, type VerifyResult } from "@agent-custody/receipts";
2
+ import { type ReceiptSummary } from "./blast.ts";
3
+ import type { Fact, Ledger } from "./ledger.ts";
4
+ export declare const ACTION_PACK_TYPE = "https://agent-custody.dev/action-pack/v0.1";
5
+ export interface ActionPack {
6
+ version: "0.1";
7
+ generatedAt: string;
8
+ receiptId: string;
9
+ /** the receipt itself, as issued */
10
+ receipt: ReceiptBundle;
11
+ /** the beliefs the agent had been shown before this call, as the ledger knows them; ids the ledger never held are listed apart */
12
+ consumed: {
13
+ facts: Fact[];
14
+ unknown: string[];
15
+ };
16
+ /** beliefs written in this call: every fact whose source receipt is this one */
17
+ written: Fact[];
18
+ /** what each written belief is today */
19
+ status: Record<string, "believed" | "retracted" | "forgotten" | "superseded">;
20
+ /** everything downstream of the written beliefs: later calls that consumed them and beliefs derived, transitively */
21
+ downstream: {
22
+ receipts: ReceiptSummary[];
23
+ derivedFacts: Fact[];
24
+ stillBelieved: Fact[];
25
+ };
26
+ /** every receipt cited by the downstream walk, by id, as the full bundle */
27
+ receipts: Record<string, ReceiptBundle>;
28
+ missingReceipts: string[];
29
+ }
30
+ export declare const decodeStatement: (b: ReceiptBundle) => ReceiptStatement;
31
+ /**
32
+ * Builds the pack. Without a ledger the receipt is explained on its own and the belief questions are answered as
33
+ * unknown rather than as nothing, so a reader can tell "nothing depended on it" from "nobody looked".
34
+ */
35
+ export declare function buildActionPack(receiptsDir: string, receiptId: string, ledger?: Ledger): Promise<ActionPack>;
36
+ export declare function signActionPack(pack: ActionPack, key: KeyPair): Envelope;
37
+ export interface ActionPackCheck {
38
+ name: string;
39
+ ok: boolean;
40
+ detail?: string;
41
+ }
42
+ export interface ActionPackVerification {
43
+ ok: boolean;
44
+ checks: ActionPackCheck[];
45
+ pack: ActionPack | null;
46
+ /** the receipt's own verification, when issuer keys were given */
47
+ receipt: VerifyResult | null;
48
+ }
49
+ export type ReceiptKeys = Pick<VerifyOptions, "issuerKeys" | "principalKeys" | "logKeys" | "upstreamKeys" | "providerSecrets">;
50
+ /** The pack's signature and digest, the receipt inside it against the gateway's keys, every downstream receipt likewise, and that the written beliefs really cite this receipt. */
51
+ export declare function verifyActionPack(envelope: Envelope, packKeys: PublicKeyRef[], keys?: ReceiptKeys): ActionPackVerification;
52
+ /** The ten questions, answered from the pack; `verification` is the receipt's own check list when keys were given. */
53
+ export declare function formatExplain(pack: ActionPack, verification: VerifyResult | null, withLedger: boolean): string;
@@ -0,0 +1,186 @@
1
+ // Explain one action. A security owner types a receipt id and gets the questions answered in the order they ask
2
+ // them: who acted, who authorized it, what was allowed, what the agent saw, what it did, why, what the evidence is,
3
+ // whether it verifies, what depended on it, and what needs reversing. The receipt answers the first eight on its own;
4
+ // the ledger answers the last two, because it knows which beliefs the call wrote and what was built on them since.
5
+ // The action pack is the same thing as one signed artefact, with every receipt it cites inside it.
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { digestOf, dsseSign, dsseVerify, verifyBundle } from "@agent-custody/receipts";
9
+ import { blastRadius, loadReceipts } from "./blast.js";
10
+ export const ACTION_PACK_TYPE = "https://agent-custody.dev/action-pack/v0.1";
11
+ export const decodeStatement = (b) => JSON.parse(Buffer.from(b.envelope.payload, "base64").toString());
12
+ function loadBundle(receiptsDir, receiptId) {
13
+ return JSON.parse(readFileSync(join(receiptsDir, `${receiptId}.json`), "utf8"));
14
+ }
15
+ /**
16
+ * Builds the pack. Without a ledger the receipt is explained on its own and the belief questions are answered as
17
+ * unknown rather than as nothing, so a reader can tell "nothing depended on it" from "nobody looked".
18
+ */
19
+ export async function buildActionPack(receiptsDir, receiptId, ledger) {
20
+ const receipt = loadBundle(receiptsDir, receiptId);
21
+ const p = decodeStatement(receipt).predicate;
22
+ const consumedIds = p.consumed?.factIds ?? [];
23
+ const consumed = { facts: [], unknown: [] };
24
+ const written = [];
25
+ const status = {};
26
+ const downstream = { receipts: [], derivedFacts: [], stillBelieved: [] };
27
+ const receipts = {};
28
+ const missingReceipts = [];
29
+ if (ledger) {
30
+ for (const id of consumedIds) {
31
+ const f = await ledger.get(id);
32
+ if (f)
33
+ consumed.facts.push(f);
34
+ else
35
+ consumed.unknown.push(id);
36
+ }
37
+ for (const f of await ledger.facts())
38
+ if (f.source.receiptId === receiptId)
39
+ written.push(f);
40
+ const summaries = loadReceipts(receiptsDir);
41
+ const seenReceipts = new Map();
42
+ const seenFacts = new Map();
43
+ const believed = new Set();
44
+ for (const f of written) {
45
+ const history = await ledger.history(f.factId);
46
+ status[f.factId] = history.some((e) => e.kind === "forget") ? "forgotten" : history.some((e) => e.kind === "retract") ? "retracted" : f.validTo !== null ? "superseded" : "believed";
47
+ const b = await blastRadius(ledger, summaries, f.factId);
48
+ for (const r of b.receipts)
49
+ seenReceipts.set(r.receiptId, r);
50
+ for (const d of b.derivedFacts)
51
+ seenFacts.set(d.factId, d);
52
+ for (const s of b.stillBelieved)
53
+ believed.add(s.factId);
54
+ }
55
+ downstream.receipts = [...seenReceipts.values()].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
56
+ downstream.derivedFacts = [...seenFacts.values()];
57
+ downstream.stillBelieved = downstream.derivedFacts.filter((f) => believed.has(f.factId));
58
+ const wanted = new Set([...downstream.receipts.map((r) => r.receiptId), ...downstream.derivedFacts.map((f) => f.source.receiptId).filter((x) => !!x)]);
59
+ for (const id of [...wanted].sort()) {
60
+ try {
61
+ receipts[id] = loadBundle(receiptsDir, id);
62
+ }
63
+ catch {
64
+ missingReceipts.push(id);
65
+ }
66
+ }
67
+ }
68
+ else {
69
+ consumed.unknown.push(...consumedIds);
70
+ }
71
+ return { version: "0.1", generatedAt: new Date().toISOString(), receiptId, receipt, consumed, written, status, downstream, receipts, missingReceipts };
72
+ }
73
+ export function signActionPack(pack, key) {
74
+ const statement = { _type: "https://in-toto.io/Statement/v1", subject: [{ name: `action-pack:${pack.receiptId}`, digest: { sha256: digestOf(pack) } }], predicateType: ACTION_PACK_TYPE, predicate: pack };
75
+ return dsseSign("application/vnd.in-toto+json", statement, key);
76
+ }
77
+ /** The pack's signature and digest, the receipt inside it against the gateway's keys, every downstream receipt likewise, and that the written beliefs really cite this receipt. */
78
+ export function verifyActionPack(envelope, packKeys, keys) {
79
+ const checks = [];
80
+ const add = (name, ok, detail) => {
81
+ checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
82
+ return ok;
83
+ };
84
+ const v = dsseVerify(envelope, packKeys);
85
+ if (!v.ok) {
86
+ add("pack signature", false, v.error);
87
+ return { ok: false, checks, pack: null, receipt: null };
88
+ }
89
+ add("pack signature", true, `keyid ${v.keyid.slice(0, 12)}`);
90
+ const st = v.payload;
91
+ if (!add("pack type", st.predicateType === ACTION_PACK_TYPE && !!st.predicate))
92
+ return { ok: false, checks, pack: null, receipt: null };
93
+ const pack = st.predicate;
94
+ add("pack digest", st.subject?.[0]?.digest?.sha256 === digestOf(pack));
95
+ let receiptResult = null;
96
+ const p = decodeStatement(pack.receipt).predicate;
97
+ add("receipt is the one the pack names", p.receiptId === pack.receiptId);
98
+ if (keys) {
99
+ const opts = { issuerKeys: keys.issuerKeys, principalKeys: keys.principalKeys ?? [], ...(keys.logKeys ? { logKeys: keys.logKeys } : {}), ...(keys.upstreamKeys ? { upstreamKeys: keys.upstreamKeys } : {}), ...(keys.providerSecrets ? { providerSecrets: keys.providerSecrets } : {}) };
100
+ receiptResult = verifyBundle(pack.receipt, opts);
101
+ add("receipt verifies", receiptResult.ok, receiptResult.ok ? `${receiptResult.checks.length} checks` : receiptResult.checks.filter((c) => !c.ok).map((c) => c.name).join(", "));
102
+ for (const [id, bundle] of Object.entries(pack.receipts)) {
103
+ const r = verifyBundle(bundle, opts);
104
+ add(`downstream receipt ${id.slice(0, 8)} verifies`, r.ok && r.statement?.predicate.receiptId === id, r.ok ? undefined : r.checks.filter((c) => !c.ok).map((c) => c.name).join(", "));
105
+ }
106
+ }
107
+ else {
108
+ add("receipts checked against the gateway's keys", false, "no issuer key given; pass --issuer-key to check the receipts inside the pack");
109
+ }
110
+ add("written beliefs cite this receipt", pack.written.every((f) => f.source.receiptId === pack.receiptId));
111
+ add("no downstream receipts missing", pack.missingReceipts.length === 0, pack.missingReceipts.length ? `${pack.missingReceipts.length} cited receipt(s) absent` : undefined);
112
+ return { ok: checks.every((c) => c.ok), checks, pack, receipt: receiptResult };
113
+ }
114
+ const decodeDelegation = (env) => {
115
+ try {
116
+ return JSON.parse(Buffer.from(env.payload, "base64").toString());
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ };
122
+ const factLine = (f) => `${f.subject} ${f.predicate}${f.forgotten ? " (erased)" : ` = ${JSON.stringify(f.value)}`} [${f.factId.slice(0, 8)}]`;
123
+ /** The ten questions, answered from the pack; `verification` is the receipt's own check list when keys were given. */
124
+ export function formatExplain(pack, verification, withLedger) {
125
+ const p = decodeStatement(pack.receipt).predicate;
126
+ const lines = [];
127
+ const row = (q, a) => lines.push(`${q.padEnd(28)} ${a}`);
128
+ const more = (a) => lines.push(`${"".padEnd(28)} ${a}`);
129
+ row("WHO", `${p.agent.id} (${p.agent.provenance}${p.issuer.kind === "sdk" ? ", self-reported by its own process" : ", named in a signed grant"})`);
130
+ const d = p.delegation ? decodeDelegation(p.delegation.envelope) : null;
131
+ if (p.principal.provenance === "attested")
132
+ row("WHO AUTHORIZED IT", `${p.principal.id}, grant signed by key ${p.principal.keyid.slice(0, 12)}${d ? `, valid ${d.issuedAt} to ${d.expiresAt}` : ""}`);
133
+ else
134
+ row("WHO AUTHORIZED IT", `${p.principal.id ?? "nobody named"} (claimed; no signed grant)`);
135
+ row("WHAT WAS ALLOWED", d?.scopes ? `tools ${d.scopes.join(", ")}` : "no grant in this receipt");
136
+ if (p.policy)
137
+ more(`policy ${p.policy.policyDigest.slice(0, 12)} decided ${p.policy.decision}${p.policy.reasons.length ? ` by ${p.policy.reasons.join(", ")}` : ""}${p.policy.errors.length ? `; errors: ${p.policy.errors.join("; ")}` : ""}`);
138
+ const facts = Object.entries(p.facts);
139
+ row("WHAT THE AGENT SAW", facts.length ? facts.map(([k, f]) => `${k} = ${JSON.stringify(f.value)} (fetched by the gateway via ${f.tool})`).join("; ") : "no facts fetched for this call");
140
+ const shown = p.consumed?.factIds ?? [];
141
+ if (!withLedger)
142
+ more(shown.length ? `${shown.length} belief(s) shown before this call; open with a ledger to see them` : "no beliefs shown before this call");
143
+ else if (shown.length === 0)
144
+ more("no beliefs shown before this call");
145
+ else {
146
+ more(`${shown.length} belief(s) shown before this call:`);
147
+ for (const f of pack.consumed.facts)
148
+ more(` ${factLine(f)}`);
149
+ for (const id of pack.consumed.unknown)
150
+ more(` ${id.slice(0, 8)} (not in this ledger)`);
151
+ }
152
+ row("WHAT IT DID", `${p.tool.name} ${JSON.stringify(p.request.args)} -> ${p.execution.status}${p.execution.status === "denied" ? `: ${p.execution.reason}` : p.execution.status === "withheld" ? `: ${p.execution.reason}` : p.execution.status === "error" ? `: ${p.execution.error}` : ""}`);
153
+ row("WHY", p.policy ? (p.policy.decision === "allow" ? `the policy permitted it${p.policy.reasons.length ? ` (${p.policy.reasons.join(", ")})` : ""}` : `the policy refused it: ${[...p.policy.reasons, ...p.policy.errors].join("; ") || "no permit matched"}`) : "no policy was evaluated");
154
+ const th = pack.receipt.treeHead.signatures[0]?.keyid ?? "?";
155
+ row("WHAT EVIDENCE", `receipt ${p.receiptId}, leaf ${pack.receipt.inclusion.leafIndex} of a log whose head is signed by ${th.slice(0, 12)}`);
156
+ if (p.authorization)
157
+ more(`authorization committed as leaf ${p.authorization.inclusion.leafIndex}, before the call was forwarded`);
158
+ if ((p.execution.status === "executed" || p.execution.status === "failed") && p.execution.upstream)
159
+ more("the upstream's own signature over the result is embedded");
160
+ row("CAN I VERIFY IT", verification ? (verification.ok ? `VERIFIED, ${verification.checks.length} checks` : `NOT VERIFIED: ${verification.checks.filter((c) => !c.ok).map((c) => c.name).join(", ")}`) : "not checked here; pass the gateway's and principal's public keys");
161
+ if (!withLedger) {
162
+ row("DID ANYTHING DEPEND ON THIS", "unknown without a ledger");
163
+ row("WHAT NEEDS REVERSAL", "unknown without a ledger");
164
+ }
165
+ else {
166
+ const w = pack.written;
167
+ if (w.length === 0)
168
+ row("DID ANYTHING DEPEND ON THIS", "this call wrote no beliefs; nothing in the ledger descends from it");
169
+ else {
170
+ row("DID ANYTHING DEPEND ON THIS", `${w.length} belief(s) written in this call, ${pack.downstream.receipts.length} later call(s) made after seeing them, ${pack.downstream.derivedFacts.length} belief(s) derived`);
171
+ for (const f of w)
172
+ more(` wrote ${factLine(f)}: ${pack.status[f.factId]}`);
173
+ for (const r of pack.downstream.receipts)
174
+ more(` then ${r.timestamp} ${r.tool} ${r.status} (receipt ${r.receiptId.slice(0, 8)})`);
175
+ }
176
+ const open = [...w.filter((f) => pack.status[f.factId] === "believed"), ...pack.downstream.stillBelieved];
177
+ if (open.length === 0)
178
+ row("WHAT NEEDS REVERSAL", w.length === 0 ? "nothing" : "nothing still believed; every belief from this call and after it is retracted, forgotten, or superseded");
179
+ else {
180
+ row("WHAT NEEDS REVERSAL", `${open.length} belief(s) still believed${pack.downstream.receipts.length ? `, and ${pack.downstream.receipts.length} later call(s) to review` : ""}`);
181
+ for (const f of open)
182
+ more(` ${factLine(f)} (space ${f.space})`);
183
+ }
184
+ }
185
+ return lines.join("\n");
186
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Ledger } from "./ledger.ts";
2
- export { JsonlStore, SqliteStore, openStore } from "./storage.ts";
3
- export type { EventStore } from "./storage.ts";
2
+ export { JsonlStore, PostgresStore, SqliteStore, openStore, selectAbout, selectFor } from "./storage.ts";
3
+ export type { EventQuery, EventStore, PostgresLike, PostgresOptions } from "./storage.ts";
4
4
  export type { AsOf, AssertEvent, AssertInput, ConfirmEvent, ConfirmInput, Fact, FactProvenance, DigestKind, ForgetEvent, ForgetInput, HoldEvent, HoldInput, LedgerEvent, SweepInput, RetractEvent, RetractInput, Source } from "./ledger.ts";
5
5
  export { AGENT_META_KEY, FACTS_META_KEY, OBSERVED_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, durationMs, retentionCutoff, serveStdio } from "./server.ts";
6
6
  export type { MemoryServerOptions } from "./server.ts";
@@ -13,6 +13,8 @@ export { factMetadata, factText, mem0Store, zepStore } from "./stores.ts";
13
13
  export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.ts";
14
14
  export { memoryHttpHandler, serveMemoryHttp } from "./http.ts";
15
15
  export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.ts";
16
+ export { ACTION_PACK_TYPE, buildActionPack, decodeStatement, formatExplain, signActionPack, verifyActionPack } from "./explain.ts";
17
+ export type { ActionPack, ActionPackCheck, ActionPackVerification, ReceiptKeys } from "./explain.ts";
16
18
  export type { CustodyPack, PackCheck, PackVerification } from "./pack.ts";
17
19
  export type { MemoryHttpOptions, RunningMemoryServer } from "./http.ts";
18
20
  export type { BlastRadius, ReceiptSummary } from "./blast.ts";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Public surface of @agent-custody/state.
2
2
  export { Ledger } from "./ledger.js";
3
- export { JsonlStore, SqliteStore, openStore } from "./storage.js";
3
+ export { JsonlStore, PostgresStore, SqliteStore, openStore, selectAbout, selectFor } from "./storage.js";
4
4
  export { AGENT_META_KEY, FACTS_META_KEY, OBSERVED_META_KEY, RECEIPT_META_KEY, SERVER_VERSION, TOOLS, createMemoryServer, durationMs, retentionCutoff, serveStdio } from "./server.js";
5
5
  export { SCENARIOS, formatReport, runAll, runScenario } from "./evals.js";
6
6
  export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.js";
@@ -9,3 +9,4 @@ export { factMetadata, factText, mem0Store, zepStore } from "./stores.js";
9
9
  export { blastRadius, formatBlastRadius, loadReceipts } from "./blast.js";
10
10
  export { memoryHttpHandler, serveMemoryHttp } from "./http.js";
11
11
  export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.js";
12
+ export { ACTION_PACK_TYPE, buildActionPack, decodeStatement, formatExplain, signActionPack, verifyActionPack } from "./explain.js";