@agent-custody/state 0.1.8 → 0.2.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.
@@ -45,6 +46,7 @@ In the gateway's config, the memory server is the upstream, and the grant names
45
46
  | `memory.confirm` | lifts a quarantined fact to attested; accepted only through the gateway | `factId` |
46
47
  | `memory.retract` | undoes a belief, keeping it visible to questions about the past | `factId`, `reason` |
47
48
  | `memory.forget` | erases the value from the ledger and every store, keeping the digest; the receipt is the certificate | `factId`, `reason` |
49
+ | `pack` (CLI) | one fact's history, receipts, holds, blast radius, and forget certificate as one signed artefact, verifiable offline | |
48
50
  | `memory.hold`, `memory.release` | legal hold: while it stands the fact cannot be forgotten by request or sweep | `factId`, `reason` |
49
51
  | `memory.sweep` | retention: forget what was learned before an instant, in a space or all, skipping held facts, reaching every store | `before`, `space`, `reason` |
50
52
  | `memory.get` | one fact by id in any state, for the gateway's policy lookups | `factId` |
@@ -84,6 +86,12 @@ A deletion demand is different from a correction. Retract keeps the record; forg
84
86
 
85
87
  **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.
86
88
 
89
+ 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.
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.
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.
94
+
87
95
  The certificate is the receipt. Through the gateway, `memory.forget` is a receipted call whose result the gateway observed, so the signed, logged receipt records that the erasure happened, who asked for it, and what the stores answered. Hand that receipt to whoever demanded the deletion; anyone with the gateway's public key can verify it.
88
96
 
89
97
  Receipts are the other place a value lives: the receipt that recorded the write carries it in its request arguments, and the receipts of reads carry it in their results, signed and in a Merkle log. The receipts package's `prune` command is retention for the log: it replaces older leaves with their hashes, so every root and every proof still verifies while the content is gone, and removes the pruned receipts' bundle files. Run it on the same schedule as the sweep.
@@ -150,9 +158,13 @@ A scenario file is `{ "version": "0.1", "scenarios": [{ "name", "ops": [...] }]
150
158
 
151
159
  ## The ledger
152
160
 
153
- `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.
161
+ `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.
162
+
163
+ **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.
164
+
165
+ 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.
154
166
 
155
- 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.
167
+ 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.
156
168
 
157
169
  The log holds these kinds of event.
158
170
 
@@ -174,10 +186,11 @@ The ledger refuses to supersede a fact that is unknown, already superseded, or r
174
186
 
175
187
  ```
176
188
  src/ledger.ts the fact record, the event kinds, as-of queries, supersession, retraction, forget, holds, sweeps
177
- src/storage.ts the event stores: JSONL (default, auditable) and SQLite (durable, shared), chosen by file extension
189
+ src/storage.ts the store interface and its three stores: JSONL (default, auditable), SQLite (indexed, one machine), Postgres (shared); chosen by path or URL
178
190
  src/server.ts the ledger as MCP tools; source and actor taken from the gateway's _meta
179
191
  src/http.ts the memory server over Streamable HTTP with bearer auth, for a shared ledger
180
- src/cli.ts agent-custody-memory serve (stdio or --http, with --retention and --forget-key-env), sweep (ledger-only or --via a gateway), eval, export, blast
192
+ src/pack.ts the custody pack: build, sign, verify, format
193
+ 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
181
194
  src/blast.ts blast radius: from receipts' consumed facts and the ledger's source receipts, forward
182
195
  src/stores.ts write-through adapters: Mem0 and Zep, and the Store interface for others
183
196
  src/evals.ts the memory-mutation harness: scenarios, scoring, report
@@ -193,10 +206,12 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
193
206
 
194
207
  **Done**
195
208
 
196
- - 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.
209
+ - 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.
197
210
  - 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.
198
211
  - Forget digests are keyed under a server-held secret, or absent on request, so an erased value cannot be guessed back from the file.
199
212
  - 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.
213
+ - The custody pack: a fact's history with receipts, holds, blast radius, and forget certificate as one signed artefact, verified as a whole.
214
+ - 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.
200
215
  - 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.
201
216
  - Value-level quarantine: a write may cite a fact the gateway fetched itself; the value must match it and the fact is then `verified`, the provenance level above attested, or the write is refused.
202
217
  - Attested executions: with a key, the memory server signs its results for the gateway's receipt, so a verifier holding its public key sees memory calls as attested.
@@ -211,7 +226,6 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
211
226
 
212
227
  **Next, in the order it pays off**
213
228
 
214
- 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).
215
- 2. Write-through adapters for Letta, LangMem, and Cognee, one per user who asks. [Issue #4](https://github.com/ch4r10t33r/agent-custody/issues/4).
216
- 3. The hosted plane, behind early access: tenanted log, then memory, then reports and a control plane. [Issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
229
+ 1. Write-through adapters for Letta, LangMem, and Cognee, one per user who asks. [Issue #4](https://github.com/ch4r10t33r/agent-custody/issues/4).
230
+ 2. 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).
217
231
 
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
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  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
+ import { buildPack, formatPack, signPack, verifyPack } from "./pack.js";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
17
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -36,13 +37,15 @@ function parseRetention(spec) {
36
37
  }
37
38
  const USAGE = `agent-custody-memory <command>
38
39
 
39
- serve --ledger <ledger.jsonl|ledger.sqlite> [--allow-direct] [--key <memory.key>] [--forget-key-env NAME] [--retention 'org=P365D,team:*=P90D']
40
+ serve --ledger <ledger.jsonl|ledger.sqlite|postgres://…> [--allow-direct] [--key <memory.key>] [--forget-key-env NAME] [--retention 'org=P365D,team:*=P90D']
40
41
  --forget-key-env names a secret kept outside the ledger; forgotten values then leave an HMAC, not a guessable hash.
41
42
  the memory server over stdio; run it as the receipts gateway's upstream.
42
43
  --key signs every result for its receipt, so executions verify as attested by this server.
43
44
  By default it refuses calls that did not come through the gateway.
44
45
  serve --ledger <ledger.jsonl> --http [--port 8790] [--host 127.0.0.1] [--token-env NAME] [--allow-direct]
45
- the same server shared over HTTP: several gateways, one ledger
46
+ the same server shared over HTTP: several gateways, one ledger.
47
+ A postgres:// ledger (needs the pg package; ?table=custody.events names the table)
48
+ is shared by every server pointed at it.
46
49
  sweep --via <gateway.json> --reason <text> [--before <ISO instant>] [--space <space>] [--no-digest]
47
50
  retention as a receipted call: runs memory.sweep through that gateway, as the principal in its grant.
48
51
  Without --before, the memory server's --retention windows decide. Put this on a timer.
@@ -52,7 +55,12 @@ const USAGE = `agent-custody-memory <command>
52
55
  runs the memory-mutation scenarios on a fresh ledger; --baseline also scores a naive
53
56
  overwrite store; --sign writes a signed report. Exits 1 if the ledger regresses.
54
57
  eval --verify <report.json> --key <pub> checks a signed report and prints its scores
55
- export --ledger <ledger.sqlite> --out <ledger.jsonl> the auditable JSONL of any ledger, one event per line
58
+ pack --ledger <ledger> --receipts <dir> --fact <factId> --out <pack.json> --sign <key>
59
+ everything about one fact as one signed artefact: history with receipts, blast radius,
60
+ holds, the forget certificate and what the stores answered. For counsel and auditors.
61
+ pack --verify <pack.json> --key <pub> [--issuer-key <pub>] [--principal-key <pub>]
62
+ checks the pack's signature and every receipt inside it
63
+ export --ledger <ledger.sqlite|postgres://…> --out <ledger.jsonl> the auditable JSONL of any ledger, one event per line; also the feed for a warehouse
56
64
  blast --ledger <ledger.jsonl> --receipts <dir> --fact <factId> [--json]
57
65
  everything that relied on a fact: later calls, derived beliefs, and whether it was retracted
58
66
  `;
@@ -64,6 +72,8 @@ async function main(argv) {
64
72
  if (!values.ledger)
65
73
  throw new Error("serve needs --ledger");
66
74
  const ledger = new Ledger(values.ledger, forgetKeyFrom(values["forget-key-env"]));
75
+ // Printed after the startup line, which supervisors and tests read first for the address.
76
+ const digestWarning = values["forget-key-env"] ? "" : "agent-custody-memory: no --forget-key-env; forgotten values leave a plain sha256, which is guessable for short values. Set a forget key, or forget with keepDigest false.";
67
77
  const identity = values.key ? loadPrivateKey(values.key) : undefined;
68
78
  const retention = values.retention ? parseRetention(values.retention) : undefined;
69
79
  const common = { requireGateway: !values["allow-direct"], ...(identity ? { identity } : {}), ...(retention ? { retention } : {}) };
@@ -72,12 +82,16 @@ async function main(argv) {
72
82
  if (values["token-env"] && !token)
73
83
  throw new Error(`serve: environment variable ${values["token-env"]} is not set`);
74
84
  const running = await serveMemoryHttp(ledger, { port: Number(values.port), host: values.host, ...common, ...(token ? { tokens: [token] } : {}) });
75
- 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"}`);
85
+ 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"}`);
86
+ if (digestWarning)
87
+ console.error(digestWarning);
76
88
  await new Promise((resolve) => process.once("SIGINT", resolve));
77
89
  await running.close();
78
90
  return 0;
79
91
  }
80
- console.error(`agent-custody-memory: ledger=${values.ledger} events=${ledger.size} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
92
+ console.error(`agent-custody-memory: ledger=${values.ledger} events=${await ledger.count()} ${values["allow-direct"] ? "direct calls allowed" : "gateway calls only"}`);
93
+ if (digestWarning)
94
+ console.error(digestWarning);
81
95
  await serveStdio(createMemoryServer(ledger, common));
82
96
  return 0;
83
97
  }
@@ -103,10 +117,12 @@ async function main(argv) {
103
117
  }
104
118
  if (!values.ledger || !values.before || !values.reason)
105
119
  throw new Error("sweep needs --ledger, --before, and --reason, or --via <gateway.json> --reason");
106
- 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 } : {}) });
120
+ const sweeper = new Ledger(values.ledger, forgetKeyFrom(values["forget-key-env"]));
121
+ 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 } : {}) });
107
122
  console.log(`forgot ${r.forgotten.length} fact(s); ${r.held.length} on hold, kept`);
108
123
  for (const f of r.forgotten)
109
124
  console.log(` ${f.factId} ${f.digestKind}${f.valueDigest ? ` ${f.valueDigest.slice(0, 12)}` : ""}`);
125
+ await sweeper.close();
110
126
  return 0;
111
127
  }
112
128
  case "eval": {
@@ -146,21 +162,51 @@ async function main(argv) {
146
162
  }
147
163
  return regressed ? 1 : 0;
148
164
  }
165
+ case "pack": {
166
+ const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, receipts: { type: "string" }, fact: { 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 }, json: { type: "boolean", default: false } } });
167
+ if (values.verify) {
168
+ if (!values.key?.length)
169
+ throw new Error("pack --verify needs --key <pub>");
170
+ const r = verifyPack(JSON.parse(readFileSync(values.verify, "utf8")), values.key.map(loadPublicKey), values["issuer-key"]?.length ? { issuerKeys: values["issuer-key"].map(loadPublicKey), principalKeys: (values["principal-key"] ?? []).map(loadPublicKey) } : undefined);
171
+ if (values.json)
172
+ console.log(JSON.stringify(r, null, 2));
173
+ else {
174
+ for (const c of r.checks)
175
+ console.log(`${c.ok ? "PASS" : "FAIL"} ${c.name}${c.detail ? ` (${c.detail})` : ""}`);
176
+ console.log(`\nRESULT: ${r.ok ? "VERIFIED" : "NOT VERIFIED"}`);
177
+ if (r.pack)
178
+ console.log("\n" + formatPack(r.pack));
179
+ }
180
+ return r.ok ? 0 : 1;
181
+ }
182
+ if (!values.ledger || !values.receipts || !values.fact || !values.out || !values.sign)
183
+ throw new Error("pack needs --ledger, --receipts, --fact, --out, and --sign");
184
+ const packLedger = new Ledger(values.ledger);
185
+ const pack = await buildPack(packLedger, values.receipts, values.fact);
186
+ await packLedger.close();
187
+ writeFileSync(values.out, JSON.stringify(signPack(pack, loadPrivateKey(values.sign)), null, 2));
188
+ console.log(formatPack(pack));
189
+ console.error(`signed pack written to ${values.out}`);
190
+ return pack.missingReceipts.length ? 1 : 0;
191
+ }
149
192
  case "export": {
150
193
  const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, out: { type: "string" } } });
151
194
  if (!values.ledger || !values.out)
152
195
  throw new Error("export needs --ledger and --out");
153
196
  const l = new Ledger(values.ledger);
154
- writeFileSync(values.out, l.export().map((e) => JSON.stringify(e)).join("\n") + "\n");
155
- console.log(`exported ${l.size} event(s) from ${l.location} to ${values.out}`);
156
- l.close();
197
+ const events = await l.export();
198
+ writeFileSync(values.out, events.map((e) => JSON.stringify(e)).join("\n") + "\n");
199
+ console.log(`exported ${events.length} event(s) from ${l.location} to ${values.out}`);
200
+ await l.close();
157
201
  return 0;
158
202
  }
159
203
  case "blast": {
160
204
  const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, receipts: { type: "string" }, fact: { type: "string" }, json: { type: "boolean", default: false } } });
161
205
  if (!values.ledger || !values.receipts || !values.fact)
162
206
  throw new Error("blast needs --ledger, --receipts, and --fact");
163
- const b = blastRadius(new Ledger(values.ledger), loadReceipts(values.receipts), values.fact);
207
+ const blastLedger = new Ledger(values.ledger);
208
+ const b = await blastRadius(blastLedger, loadReceipts(values.receipts), values.fact);
209
+ await blastLedger.close();
164
210
  console.log(values.json ? JSON.stringify(b, null, 2) : formatBlastRadius(b, values.fact));
165
211
  return b.fact ? 0 : 1;
166
212
  }
@@ -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
  }
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";
@@ -12,6 +12,8 @@ export type { EvalReportPredicate, ReportCheck, ScenarioFile } from "./evals-fil
12
12
  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
+ export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.ts";
16
+ export type { CustodyPack, PackCheck, PackVerification } from "./pack.ts";
15
17
  export type { MemoryHttpOptions, RunningMemoryServer } from "./http.ts";
16
18
  export type { BlastRadius, ReceiptSummary } from "./blast.ts";
17
- export type { Mem0Like, Mem0Options, Store, ZepLike, ZepOptions } from "./stores.ts";
19
+ export type { Mem0Like, Mem0Options, RemovalOutcome, Store, ZepLike, ZepOptions } from "./stores.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";
@@ -8,3 +8,4 @@ export { EVAL_REPORT_TYPE, ScenarioFileSchema, loadScenarios, signReport, verify
8
8
  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
+ export { PACK_TYPE, buildPack, formatPack, signPack, verifyPack } from "./pack.js";
package/dist/ledger.d.ts CHANGED
@@ -155,13 +155,14 @@ export interface AsOf {
155
155
  include?: "attested" | "verified" | "all";
156
156
  }
157
157
  export declare class Ledger {
158
- private readonly events;
159
158
  private readonly store;
160
159
  private readonly now;
161
160
  private readonly forgetKey;
162
161
  /**
163
- * `location` is a path: JSONL by default, SQLite when it ends in .sqlite or .db; or pass a store.
164
- * forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a plain hash.
162
+ * `location` is a path (JSONL by default, SQLite when it ends in .sqlite or .db) or a postgres:// URL; or pass a
163
+ * store. The ledger holds no events itself: every question is a query to the store, so a shared store means a
164
+ * shared ledger. forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a
165
+ * plain hash.
165
166
  */
166
167
  constructor(location: string | EventStore, opts?: {
167
168
  now?: () => Date;
@@ -170,37 +171,47 @@ export declare class Ledger {
170
171
  /** Where the events live, for reports. */
171
172
  get location(): string;
172
173
  /** Every event in order, for export. */
173
- export(): LedgerEvent[];
174
- close(): void;
175
- get size(): number;
174
+ export(): Promise<LedgerEvent[]>;
175
+ close(): Promise<void>;
176
+ count(): Promise<number>;
177
+ /** Every space with at least one fact. */
178
+ spaces(): Promise<string[]>;
176
179
  /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
177
- validateAssert(input: AssertInput): void;
178
- assert(input: AssertInput): AssertEvent;
179
- retract(input: RetractInput): RetractEvent;
180
- confirm(input: ConfirmInput): ConfirmEvent;
181
- /**
182
- * Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
183
- * place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
184
- * stays: who wrote it, when, from which receipt, and now who erased it and why.
185
- */
180
+ validateAssert(input: AssertInput): Promise<void>;
181
+ assert(input: AssertInput): Promise<AssertEvent>;
182
+ retract(input: RetractInput): Promise<RetractEvent>;
183
+ confirm(input: ConfirmInput): Promise<ConfirmEvent>;
186
184
  /** Whether a legal hold currently stands on the fact. */
187
- held(factId: string): boolean;
188
- hold(input: HoldInput): HoldEvent;
189
- release(input: HoldInput): HoldEvent;
185
+ held(factId: string): Promise<boolean>;
186
+ hold(input: HoldInput): Promise<HoldEvent>;
187
+ release(input: HoldInput): Promise<HoldEvent>;
188
+ /** The facts the ledger learned of before an instant, in one space or all, that have not been forgotten: what a retention sweep decides about. */
189
+ learnedBefore(before: string, space?: string): Promise<Fact[]>;
190
190
  /** Retention: forgets every fact the ledger learned of before the cutoff, in one space or all, skipping held and already-forgotten facts. Returns what it forgot and what it skipped. */
191
- sweep(input: SweepInput): {
191
+ sweep(input: SweepInput): Promise<{
192
192
  forgotten: ForgetEvent[];
193
193
  held: string[];
194
- };
195
- forget(input: ForgetInput): ForgetEvent;
194
+ }>;
195
+ /**
196
+ * Erases a fact's value from the store itself, keeping its digest, and stops believing it. The store rewrites the
197
+ * event in place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about
198
+ * the fact stays: who wrote it, when, from which receipt, and now who erased it and why. With compact false the
199
+ * store's reclaim step is left to the caller, for a batch of forgets followed by one compact().
200
+ */
201
+ forget(input: ForgetInput & {
202
+ compact?: boolean;
203
+ }): Promise<ForgetEvent>;
204
+ /** Reclaims whatever the store may still hold of erased values; forget and sweep do this themselves unless told not to. */
205
+ compact(): Promise<void>;
196
206
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
197
- asOf(q?: AsOf): Fact[];
207
+ asOf(q?: AsOf): Promise<Fact[]>;
208
+ /** One fact by id, whatever its state, with supersession applied; undefined when the ledger never held it. */
209
+ get(factId: string): Promise<Fact | undefined>;
198
210
  /** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
199
- facts(): Fact[];
211
+ facts(): Promise<Fact[]>;
200
212
  /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
201
- history(factId: string): LedgerEvent[];
213
+ history(factId: string): Promise<LedgerEvent[]>;
202
214
  private confirmedAt;
203
215
  private factById;
204
216
  private retractedAt;
205
- private append;
206
217
  }