@agent-custody/state 0.5.5 → 0.5.6
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 +9 -3
- package/dist/cli.js +26 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/review.d.ts +42 -0
- package/dist/review.js +150 -0
- package/dist/server.js +6 -3
- package/dist/stores.d.ts +22 -0
- package/dist/stores.js +40 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -142,9 +142,11 @@ WHAT NEEDS REVERSAL 2 belief(s) still believed, and 3 later call(s) to
|
|
|
142
142
|
|
|
143
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
144
|
|
|
145
|
+
**The review page.** `agent-custody-memory review --receipts receipts --ledger ledger.sqlite --issuer-key keys/gateway.pub --principal-key keys/principal.pub` serves the same answers as pages on loopback: an index of every receipt with when, tool, outcome, agent, principal, producer, and its verdict, and one page per receipt with the ten answers, the verification report, and the receipt itself to download. `--out <dir>` writes the same as static files for a case file or a shared drive. It runs where the receipts are, because nobody else holds them; it has no login of its own, so expose it only behind one you already have.
|
|
146
|
+
|
|
145
147
|
## Write-through to the stores you already use
|
|
146
148
|
|
|
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.
|
|
149
|
+
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. For teams whose recall is a pgvector table, `pgvectorStore` writes the fact's text, its embedding from the function you already use, and its custody metadata as one row keyed by the fact id, and verifies a removal with the same nearest-neighbour query a retrieval call runs.
|
|
148
150
|
|
|
149
151
|
```ts
|
|
150
152
|
import { MemoryClient } from "mem0ai";
|
|
@@ -154,6 +156,7 @@ import { Ledger, createMemoryServer, mem0Store, zepStore } from "@agent-custody/
|
|
|
154
156
|
const stores = [
|
|
155
157
|
mem0Store(new MemoryClient({ apiKey: process.env.MEM0_API_KEY! }), { userId: "user_42" }), // infer is off: the memory is the fact, verbatim
|
|
156
158
|
zepStore(new ZepClient({ apiKey: process.env.ZEP_API_KEY! }), { userId: "user_42" }), // or { graphId } for a shared graph
|
|
159
|
+
pgvectorStore(pool, { embed, dimensions: 1536, table: "agent_memories" }), // your Postgres, your embedding function
|
|
157
160
|
];
|
|
158
161
|
createMemoryServer(new Ledger("./ledger.jsonl"), { stores });
|
|
159
162
|
```
|
|
@@ -213,10 +216,11 @@ src/storage.ts the store interface and its three stores: JSONL (default, audita
|
|
|
213
216
|
src/server.ts the ledger as MCP tools; source and actor taken from the gateway's _meta
|
|
214
217
|
src/http.ts the memory server over Streamable HTTP with bearer auth, for a shared ledger
|
|
215
218
|
src/explain.ts one action explained: the ten answers from a receipt and the ledger, and the signed action pack
|
|
219
|
+
src/review.ts the review page: the explain output as pages, served on loopback or written as files
|
|
216
220
|
src/pack.ts the custody pack: build, sign, verify, format
|
|
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
|
|
221
|
+
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, review, pack, export, blast
|
|
218
222
|
src/blast.ts blast radius: from receipts' consumed facts and the ledger's source receipts, forward
|
|
219
|
-
src/stores.ts write-through adapters: Mem0 and
|
|
223
|
+
src/stores.ts write-through adapters: Mem0, Zep, and pgvector, and the Store interface for others
|
|
220
224
|
src/evals.ts the memory-mutation harness: scenarios, scoring, report
|
|
221
225
|
src/evals-ledger.ts the ledger and a naive overwrite store behind the harness interface
|
|
222
226
|
src/evals-file.ts scenario files, validated; signed eval reports and their verification
|
|
@@ -234,6 +238,7 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
|
|
|
234
238
|
- 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.
|
|
235
239
|
- Forget digests are keyed under a server-held secret, or absent on request, so an erased value cannot be guessed back from the file.
|
|
236
240
|
- 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.
|
|
241
|
+
- The review page: the explain output as pages for the reviewer who will not open a terminal, an index of every receipt with its verdict and one page per receipt, served on loopback or written as static files.
|
|
237
242
|
- 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.
|
|
238
243
|
- The custody pack: a fact's history with receipts, holds, blast radius, and forget certificate as one signed artefact, verified as a whole.
|
|
239
244
|
- 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.
|
|
@@ -244,6 +249,7 @@ tsconfig.build.json emits dist/ for consumers; the repo itself runs the .ts dir
|
|
|
244
249
|
- Certified forget: `memory.forget` erases a value from the ledger file keeping its digest, stops believing it, removes it from every store, and reports exactly what happened; the gateway's receipt of that call is the certificate.
|
|
245
250
|
- Policy over provenance: the gateway looks up the fact a write supersedes or a retraction targets, so policy decides on its space, actor, and provenance; a claimed fact can be displaced, an attested org fact cannot.
|
|
246
251
|
- Consumed facts and blast radius: the memory server declares the facts it serves, the gateway records them on every later receipt, and `blast` walks from a fact to every downstream call and derived belief, transitively, with its retraction status.
|
|
252
|
+
- Write-through to pgvector: the fact's text, embedding, and custody metadata as one row in the Postgres a team already runs, keyed by the fact id; removal verified by the same nearest-neighbour query recall runs; tested against Postgres with the extension in Docker.
|
|
247
253
|
- Write-through adapters for Mem0 and Zep: every write lands in the store with custody metadata, the store id is recorded on the fact, retractions reach the store, and failures are ordered so nothing is half-recorded.
|
|
248
254
|
- The eval CLI: built-in or custom scenario files, the naive baseline beside the ledger, a signed report a reviewer verifies, and a non-zero exit on regression, for cron.
|
|
249
255
|
- The memory-mutation eval harness: stale reads, contradictions, blast radius, and correct reads over scripted incidents, scored the same way for the ledger and for anything behind the same interface.
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
15
|
import { buildActionPack, formatExplain, signActionPack, verifyActionPack } from "./explain.js";
|
|
16
|
+
import { serveReview, writeReview } from "./review.js";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
18
19
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -68,6 +69,11 @@ const USAGE = `agent-custody-memory <command>
|
|
|
68
69
|
explain ... --out <action.json> --sign <key> the same as one signed action pack, every downstream receipt inside
|
|
69
70
|
explain --verify <action.json> --key <pub> [--issuer-key <pub>] [--principal-key <pub>] [--log-key <pub>]
|
|
70
71
|
checks the pack, the receipt inside it, and every downstream receipt
|
|
72
|
+
review --receipts <dir> [--ledger <ledger>] [--issuer-key <pub>] [--principal-key <pub>] [--log-key <pub>] [--log-id <id>] [--port 8791] [--host 127.0.0.1] [--title <text>]
|
|
73
|
+
the explain output as pages, for the reviewer who will not open a terminal: an index of every
|
|
74
|
+
receipt with its verdict, one page per receipt with the ten answers and the verification report.
|
|
75
|
+
Serves on loopback; put it behind your own login if you expose it.
|
|
76
|
+
review ... --out <dir> the same as static files, for a case file or a shared drive
|
|
71
77
|
export --ledger <ledger.sqlite|postgres://…> --out <ledger.jsonl> the auditable JSONL of any ledger, one event per line; also the feed for a warehouse
|
|
72
78
|
blast --ledger <ledger.jsonl> --receipts <dir> --fact <factId> [--json]
|
|
73
79
|
everything that relied on a fact: later calls, derived beliefs, and whether it was retracted
|
|
@@ -233,6 +239,26 @@ async function main(argv) {
|
|
|
233
239
|
console.log(formatExplain(pack, verification, !!ledger));
|
|
234
240
|
return verification && !verification.ok ? 1 : pack.missingReceipts.length ? 1 : 0;
|
|
235
241
|
}
|
|
242
|
+
case "review": {
|
|
243
|
+
const { values } = parseArgs({ args: rest, options: { receipts: { type: "string" }, ledger: { type: "string" }, "issuer-key": { type: "string", multiple: true }, "principal-key": { type: "string", multiple: true }, "log-key": { type: "string", multiple: true }, "log-id": { type: "string" }, port: { type: "string", default: "8791" }, host: { type: "string", default: "127.0.0.1" }, title: { type: "string" }, out: { type: "string" } } });
|
|
244
|
+
if (!values.receipts)
|
|
245
|
+
throw new Error("review needs --receipts");
|
|
246
|
+
const keys = 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) } : {}), ...(values["log-id"] ? { logId: values["log-id"] } : {}) } : undefined;
|
|
247
|
+
const ledger = values.ledger ? new Ledger(values.ledger) : undefined;
|
|
248
|
+
const o = { receiptsDir: values.receipts, ...(ledger ? { ledger } : {}), ...(keys ? { keys } : {}), ...(values.title ? { title: values.title } : {}) };
|
|
249
|
+
if (values.out) {
|
|
250
|
+
const r = await writeReview(o, values.out);
|
|
251
|
+
console.log(`wrote ${r.receipts} receipt page(s) and the index to ${values.out}`);
|
|
252
|
+
await ledger?.close();
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
const running = await serveReview(o, { port: Number(values.port), host: values.host });
|
|
256
|
+
console.error(`agent-custody-memory review: ${running.url} receipts=${values.receipts}${ledger ? ` ledger=${values.ledger}` : ""}${keys ? " verifying" : " not verifying (no keys given)"}`);
|
|
257
|
+
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
258
|
+
await running.close();
|
|
259
|
+
await ledger?.close();
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
236
262
|
case "export": {
|
|
237
263
|
const { values } = parseArgs({ args: rest, options: { ledger: { type: "string" }, out: { type: "string" } } });
|
|
238
264
|
if (!values.ledger || !values.out)
|
package/dist/index.d.ts
CHANGED
|
@@ -9,13 +9,15 @@ export type { MemoryUnderTest, Op, Report, Scenario, ScenarioScore } from "./eva
|
|
|
9
9
|
export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.ts";
|
|
10
10
|
export { EVAL_REPORT_TYPE, ScenarioFileSchema, loadScenarios, signReport, verifyReport } from "./evals-file.ts";
|
|
11
11
|
export type { EvalReportPredicate, ReportCheck, ScenarioFile } from "./evals-file.ts";
|
|
12
|
-
export { factMetadata, factText, mem0Store, zepStore } from "./stores.ts";
|
|
12
|
+
export { factMetadata, factText, mem0Store, pgvectorStore, 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
16
|
export { ACTION_PACK_TYPE, buildActionPack, decodeStatement, formatExplain, signActionPack, verifyActionPack } from "./explain.ts";
|
|
17
|
+
export { listReceipts, renderIndex, renderReceipt, reviewHandler, serveReview, writeReview } from "./review.ts";
|
|
18
|
+
export type { ReceiptRow, ReviewOptions, RunningReview } from "./review.ts";
|
|
17
19
|
export type { ActionPack, ActionPackCheck, ActionPackVerification, ReceiptKeys } from "./explain.ts";
|
|
18
20
|
export type { CustodyPack, PackCheck, PackVerification } from "./pack.ts";
|
|
19
21
|
export type { MemoryHttpOptions, RunningMemoryServer } from "./http.ts";
|
|
20
22
|
export type { BlastRadius, ReceiptSummary } from "./blast.ts";
|
|
21
|
-
export type { Mem0Like, Mem0Options, RemovalOutcome, Store, ZepLike, ZepOptions } from "./stores.ts";
|
|
23
|
+
export type { Mem0Like, Mem0Options, PgLike, PgvectorOptions, RemovalOutcome, Store, ZepLike, ZepOptions } from "./stores.ts";
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,9 @@ export { AGENT_META_KEY, FACTS_META_KEY, OBSERVED_META_KEY, RECEIPT_META_KEY, SE
|
|
|
5
5
|
export { SCENARIOS, formatReport, runAll, runScenario } from "./evals.js";
|
|
6
6
|
export { ledgerUnderTest, overwriteStoreUnderTest } from "./evals-ledger.js";
|
|
7
7
|
export { EVAL_REPORT_TYPE, ScenarioFileSchema, loadScenarios, signReport, verifyReport } from "./evals-file.js";
|
|
8
|
-
export { factMetadata, factText, mem0Store, zepStore } from "./stores.js";
|
|
8
|
+
export { factMetadata, factText, mem0Store, pgvectorStore, 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
12
|
export { ACTION_PACK_TYPE, buildActionPack, decodeStatement, formatExplain, signActionPack, verifyActionPack } from "./explain.js";
|
|
13
|
+
export { listReceipts, renderIndex, renderReceipt, reviewHandler, serveReview, writeReview } from "./review.js";
|
package/dist/ledger.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export interface HoldEvent {
|
|
|
96
96
|
}
|
|
97
97
|
export type LedgerEvent = AssertEvent | RetractEvent | ConfirmEvent | ForgetEvent | HoldEvent;
|
|
98
98
|
export interface AssertInput {
|
|
99
|
+
/** the fact's id, when the caller had to know it before the append, as the memory server does for its stores; default a fresh UUID */
|
|
100
|
+
factId?: string;
|
|
99
101
|
subject: string;
|
|
100
102
|
predicate: string;
|
|
101
103
|
value: unknown;
|
package/dist/ledger.js
CHANGED
package/dist/review.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { type VerifyOptions, type VerifyResult } from "@agent-custody/receipts";
|
|
3
|
+
import { type ActionPack } from "./explain.ts";
|
|
4
|
+
import type { Ledger } from "./ledger.ts";
|
|
5
|
+
export interface ReviewOptions {
|
|
6
|
+
receiptsDir: string;
|
|
7
|
+
ledger?: Ledger;
|
|
8
|
+
/** with these, every receipt page carries its verification report */
|
|
9
|
+
keys?: Pick<VerifyOptions, "issuerKeys" | "principalKeys" | "logKeys" | "logId">;
|
|
10
|
+
/** shown in the page header, e.g. the team or the deployment */
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReceiptRow {
|
|
14
|
+
receiptId: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
tool: string;
|
|
17
|
+
status: string;
|
|
18
|
+
agent: string;
|
|
19
|
+
principal: string | null;
|
|
20
|
+
issuer: string;
|
|
21
|
+
verified: boolean | null;
|
|
22
|
+
}
|
|
23
|
+
/** Every receipt in the directory, newest first, with its verdict when keys were given. Damaged files are skipped. */
|
|
24
|
+
export declare function listReceipts(o: ReviewOptions): ReceiptRow[];
|
|
25
|
+
/** The index: one row per receipt, newest first. */
|
|
26
|
+
export declare function renderIndex(rows: ReceiptRow[], o: ReviewOptions, link: (id: string) => string): string;
|
|
27
|
+
/** One receipt's page: the ten answers, the verification report, and the receipt itself. */
|
|
28
|
+
export declare function renderReceipt(pack: ActionPack, verification: VerifyResult | null, o: ReviewOptions, backLink: string, rawLink: string): string;
|
|
29
|
+
/** Serves the review on loopback: / is the index, /r/<id> a receipt, /r/<id>.json the bundle. */
|
|
30
|
+
export declare function reviewHandler(o: ReviewOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
31
|
+
export interface RunningReview {
|
|
32
|
+
url: string;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare function serveReview(o: ReviewOptions, opts: {
|
|
36
|
+
port: number;
|
|
37
|
+
host?: string;
|
|
38
|
+
}): Promise<RunningReview>;
|
|
39
|
+
/** Writes the same pages as static files: index.html, and r/<id>.html with r/<id>.json beside it. For a case file or a shared drive. */
|
|
40
|
+
export declare function writeReview(o: ReviewOptions, outDir: string): Promise<{
|
|
41
|
+
receipts: number;
|
|
42
|
+
}>;
|
package/dist/review.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The review page: the explain command for people who will not open a terminal. It serves, or writes as static
|
|
2
|
+
// files, one page per receipt with the ten answers and the verification report, and an index of every receipt in
|
|
3
|
+
// the directory. It runs where the receipts are, on the customer's machine, because nobody else holds them; the
|
|
4
|
+
// hosted log has only their hashes. No framework, no outside requests, and no authentication of its own: bind it
|
|
5
|
+
// to loopback, or put it behind whatever already guards the machine.
|
|
6
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { verifyBundle } from "@agent-custody/receipts";
|
|
10
|
+
import { buildActionPack, decodeStatement, formatExplain } from "./explain.js";
|
|
11
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
12
|
+
/** Every receipt in the directory, newest first, with its verdict when keys were given. Damaged files are skipped. */
|
|
13
|
+
export function listReceipts(o) {
|
|
14
|
+
const rows = [];
|
|
15
|
+
for (const f of readdirSync(o.receiptsDir)) {
|
|
16
|
+
if (!f.endsWith(".json") || f.endsWith(".authorization.json"))
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
const bundle = JSON.parse(readFileSync(join(o.receiptsDir, f), "utf8"));
|
|
20
|
+
const p = decodeStatement(bundle).predicate;
|
|
21
|
+
const verified = o.keys ? verifyBundle(bundle, { issuerKeys: o.keys.issuerKeys, principalKeys: o.keys.principalKeys ?? [], ...(o.keys.logKeys ? { logKeys: o.keys.logKeys } : {}), ...(o.keys.logId ? { logId: o.keys.logId } : {}) }).ok : null;
|
|
22
|
+
rows.push({ receiptId: p.receiptId, timestamp: p.timestamp, tool: p.tool.name, status: p.execution.status, agent: p.agent.id, principal: p.principal.id, issuer: p.issuer.kind, verified });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// not a bundle; the index is of receipts
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return rows.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
29
|
+
}
|
|
30
|
+
const STYLE = `
|
|
31
|
+
:root { color-scheme: light dark; --ink: #1b2430; --ink2: #5b6b7a; --line: #d7dfe5; --bg: #fafbfc; --panel: #ffffff; --accent: #0f6e63; --bad: #b3261e; --mono: ui-monospace, Menlo, monospace; }
|
|
32
|
+
@media (prefers-color-scheme: dark) { :root { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #4fc3b0; --bad: #ff8a80; } }
|
|
33
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
|
|
34
|
+
main { max-width: 74rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
35
|
+
h1 { font-size: 1.4rem; margin: 0 0 .25rem; } h2 { font-size: 1.05rem; margin: 1.75rem 0 .6rem; }
|
|
36
|
+
.sub { color: var(--ink2); margin: 0 0 1.25rem; }
|
|
37
|
+
a { color: var(--accent); }
|
|
38
|
+
table { border-collapse: collapse; width: 100%; font-size: .93rem; }
|
|
39
|
+
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; }
|
|
40
|
+
th { font-size: .78rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink2); }
|
|
41
|
+
code, pre, .mono { font-family: var(--mono); font-size: .88em; }
|
|
42
|
+
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: .9rem 1rem; overflow-x: auto; white-space: pre-wrap; }
|
|
43
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: .45rem 1.25rem; margin: 0; }
|
|
44
|
+
dt { font-size: .78rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink2); padding-top: .15rem; }
|
|
45
|
+
dd { margin: 0; white-space: pre-wrap; }
|
|
46
|
+
.ok { color: var(--accent); font-weight: 600; } .bad { color: var(--bad); font-weight: 600; } .muted { color: var(--ink2); }
|
|
47
|
+
.wrap { overflow-x: auto; }
|
|
48
|
+
@media (max-width: 40rem) { dl { grid-template-columns: 1fr; } }
|
|
49
|
+
`;
|
|
50
|
+
const page = (title, body) => `<!doctype html>
|
|
51
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
52
|
+
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:">
|
|
53
|
+
<title>${esc(title)}</title>
|
|
54
|
+
<style>${STYLE}</style>
|
|
55
|
+
<main>${body}</main>
|
|
56
|
+
`;
|
|
57
|
+
/** The index: one row per receipt, newest first. */
|
|
58
|
+
export function renderIndex(rows, o, link) {
|
|
59
|
+
const withKeys = !!o.keys;
|
|
60
|
+
const verdict = (r) => (r.verified === null ? '<span class="muted">not checked</span>' : r.verified ? '<span class="ok">verified</span>' : '<span class="bad">NOT VERIFIED</span>');
|
|
61
|
+
const body = `
|
|
62
|
+
<h1>${esc(o.title ?? "Receipts")}</h1>
|
|
63
|
+
<p class="sub">${rows.length} receipt(s) in ${esc(o.receiptsDir)}${withKeys ? ", each checked against the keys given" : ", not checked: start the review with the gateway's and principal's public keys to verify"}${o.ledger ? "; beliefs from the ledger" : "; no ledger, so what depended on a call is not known here"}.</p>
|
|
64
|
+
<div class="wrap"><table><thead><tr><th>when</th><th>tool</th><th>outcome</th><th>agent</th><th>principal</th><th>producer</th><th>verification</th><th>receipt</th></tr></thead><tbody>
|
|
65
|
+
${rows.map((r) => `<tr><td>${esc(r.timestamp.replace("T", " ").slice(0, 19))}</td><td><code>${esc(r.tool)}</code></td><td>${esc(r.status)}</td><td>${esc(r.agent)}</td><td>${esc(r.principal ?? "")}</td><td>${esc(r.issuer)}</td><td>${verdict(r)}</td><td><a class="mono" href="${esc(link(r.receiptId))}">${esc(r.receiptId.slice(0, 8))}</a></td></tr>`).join("\n")}
|
|
66
|
+
</tbody></table></div>
|
|
67
|
+
<p class="muted">A gateway receipt was enforced outside the agent's process; an SDK receipt is the agent's own report. The verification column is the offline check any auditor can repeat with the same public keys.</p>`;
|
|
68
|
+
return page(o.title ?? "Receipts", body);
|
|
69
|
+
}
|
|
70
|
+
/** The explain text as a definition list: question, answer, continuation lines kept with their answer. */
|
|
71
|
+
function explainHtml(text) {
|
|
72
|
+
const items = [];
|
|
73
|
+
for (const line of text.split("\n")) {
|
|
74
|
+
const q = line.slice(0, 28).trim();
|
|
75
|
+
const a = line.slice(28).trimEnd();
|
|
76
|
+
if (q && /^[A-Z][A-Z ?]+$/.test(q))
|
|
77
|
+
items.push({ q, a: [a] });
|
|
78
|
+
else if (items.length)
|
|
79
|
+
items[items.length - 1].a.push(a);
|
|
80
|
+
}
|
|
81
|
+
return `<dl>${items.map((i) => `<dt>${esc(i.q)}</dt><dd>${esc(i.a.join("\n"))}</dd>`).join("")}</dl>`;
|
|
82
|
+
}
|
|
83
|
+
/** One receipt's page: the ten answers, the verification report, and the receipt itself. */
|
|
84
|
+
export function renderReceipt(pack, verification, o, backLink, rawLink) {
|
|
85
|
+
const p = decodeStatement(pack.receipt).predicate;
|
|
86
|
+
const report = verification ? verification.checks.map((c) => `${c.ok ? "PASS" : "FAIL"} ${c.name}${c.detail ? ` (${c.detail})` : ""}`).join("\n") + `\n\nRESULT: ${verification.ok ? "VERIFIED" : "NOT VERIFIED"}` : "Not checked: start the review with --issuer-key and --principal-key to verify every receipt here.";
|
|
87
|
+
const body = `
|
|
88
|
+
<p><a href="${esc(backLink)}">All receipts</a></p>
|
|
89
|
+
<h1>${esc(p.tool.name)} <span class="muted">${esc(p.execution.status)}</span></h1>
|
|
90
|
+
<p class="sub">${esc(p.timestamp.replace("T", " ").slice(0, 19))} · receipt <span class="mono">${esc(p.receiptId)}</span> · ${verification ? (verification.ok ? '<span class="ok">verified</span>' : '<span class="bad">NOT VERIFIED</span>') : '<span class="muted">not checked</span>'}</p>
|
|
91
|
+
<h2>What this action is</h2>
|
|
92
|
+
${explainHtml(formatExplain(pack, verification, !!o.ledger))}
|
|
93
|
+
<h2>Verification</h2>
|
|
94
|
+
<pre>${esc(report)}</pre>
|
|
95
|
+
<h2>The receipt</h2>
|
|
96
|
+
<p><a href="${esc(rawLink)}">The bundle as issued</a>, which any verifier checks with <code>agent-custody verify</code> and the public keys.</p>`;
|
|
97
|
+
return page(`${p.tool.name} · ${p.receiptId.slice(0, 8)}`, body);
|
|
98
|
+
}
|
|
99
|
+
function verifyOf(bundle, o) {
|
|
100
|
+
return o.keys ? verifyBundle(bundle, { issuerKeys: o.keys.issuerKeys, principalKeys: o.keys.principalKeys ?? [], ...(o.keys.logKeys ? { logKeys: o.keys.logKeys } : {}), ...(o.keys.logId ? { logId: o.keys.logId } : {}) }) : null;
|
|
101
|
+
}
|
|
102
|
+
/** Serves the review on loopback: / is the index, /r/<id> a receipt, /r/<id>.json the bundle. */
|
|
103
|
+
export function reviewHandler(o) {
|
|
104
|
+
return async (req, res) => {
|
|
105
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
106
|
+
const send = (status, type, body) => {
|
|
107
|
+
res.writeHead(status, { "content-type": type, "cache-control": "no-store", "x-frame-options": "DENY", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:" });
|
|
108
|
+
res.end(body);
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
if (url.pathname === "/")
|
|
112
|
+
return send(200, "text/html; charset=utf-8", renderIndex(listReceipts(o), o, (id) => `/r/${id}`));
|
|
113
|
+
const m = /^\/r\/([0-9a-f-]{36})(\.json)?$/.exec(url.pathname);
|
|
114
|
+
if (!m)
|
|
115
|
+
return send(404, "text/plain", "not found");
|
|
116
|
+
if (m[2])
|
|
117
|
+
return send(200, "application/json", readFileSync(join(o.receiptsDir, `${m[1]}.json`), "utf8"));
|
|
118
|
+
const pack = await buildActionPack(o.receiptsDir, m[1], o.ledger);
|
|
119
|
+
return send(200, "text/html; charset=utf-8", renderReceipt(pack, verifyOf(pack.receipt, o), o, "/", `/r/${m[1]}.json`));
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
return send(e.code === "ENOENT" ? 404 : 500, "text/plain", e instanceof Error ? e.message : String(e));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function serveReview(o, opts) {
|
|
127
|
+
const host = opts.host ?? "127.0.0.1";
|
|
128
|
+
const handler = reviewHandler(o);
|
|
129
|
+
const server = createServer((req, res) => {
|
|
130
|
+
void handler(req, res);
|
|
131
|
+
});
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
server.listen(opts.port, host, () => {
|
|
134
|
+
const { port } = server.address();
|
|
135
|
+
resolve({ url: `http://${host}:${port}/`, close: () => new Promise((r) => server.close(() => r())) });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Writes the same pages as static files: index.html, and r/<id>.html with r/<id>.json beside it. For a case file or a shared drive. */
|
|
140
|
+
export async function writeReview(o, outDir) {
|
|
141
|
+
mkdirSync(join(outDir, "r"), { recursive: true });
|
|
142
|
+
const rows = listReceipts(o);
|
|
143
|
+
writeFileSync(join(outDir, "index.html"), renderIndex(rows, o, (id) => `r/${id}.html`));
|
|
144
|
+
for (const r of rows) {
|
|
145
|
+
const pack = await buildActionPack(o.receiptsDir, r.receiptId, o.ledger);
|
|
146
|
+
writeFileSync(join(outDir, "r", `${r.receiptId}.html`), renderReceipt(pack, verifyOf(pack.receipt, o), o, "../index.html", `${r.receiptId}.json`));
|
|
147
|
+
writeFileSync(join(outDir, "r", `${r.receiptId}.json`), JSON.stringify(pack.receipt, null, 2));
|
|
148
|
+
}
|
|
149
|
+
return { receipts: rows.length };
|
|
150
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// call's _meta, which receipt it is and who the attested grant says is calling. Those become the fact's source and
|
|
4
4
|
// actor; a caller cannot supply them. Run it directly and the source is null and the actor is whatever the caller
|
|
5
5
|
// claims, which is recorded as such.
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
9
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -200,13 +201,15 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
200
201
|
}
|
|
201
202
|
const input = { subject: a.subject, predicate: a.predicate, value: a.value ?? null, space: a.space, actor: actorFor(a.actor), source: { receiptId }, provenance: writeProvenance, ...(a.validFrom ? { validFrom: a.validFrom } : {}), ...(a.confidence !== undefined ? { confidence: a.confidence } : {}), ...(a.supersedes ? { supersedes: a.supersedes } : {}) };
|
|
202
203
|
// The stores are written first, so their ids can be recorded on the fact; the ledger's checks run beforehand
|
|
203
|
-
// so a write the ledger would refuse never reaches a store.
|
|
204
|
+
// so a write the ledger would refuse never reaches a store. The fact id is chosen here, before the stores
|
|
205
|
+
// see the fact, so the metadata they keep names the real id and a store may key its row by it.
|
|
204
206
|
await ledger.validateAssert(input);
|
|
205
207
|
const external = {};
|
|
206
|
-
const
|
|
208
|
+
const factId = randomUUID();
|
|
209
|
+
const preview = { ...input, factId, validFrom: input.validFrom ?? new Date().toISOString(), validTo: null, confidence: input.confidence ?? null };
|
|
207
210
|
for (const store of opts.stores ?? [])
|
|
208
211
|
external[store.name] = await store.put(preview);
|
|
209
|
-
const ev = await ledger.assert({ ...input, external });
|
|
212
|
+
const ev = await ledger.assert({ ...input, factId, external });
|
|
210
213
|
return json({ fact: ev.fact, eventId: ev.eventId, txTime: ev.txTime, supersedes: ev.supersedes });
|
|
211
214
|
}
|
|
212
215
|
case "memory.read": {
|
package/dist/stores.d.ts
CHANGED
|
@@ -82,3 +82,25 @@ export type ZepOptions = {
|
|
|
82
82
|
userId?: undefined;
|
|
83
83
|
};
|
|
84
84
|
export declare function zepStore(client: ZepLike, opts: ZepOptions): Store;
|
|
85
|
+
/** What the pgvector adapter needs from a client: the query method of a pg Pool or of PGlite. */
|
|
86
|
+
export interface PgLike {
|
|
87
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
88
|
+
rows: unknown[];
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
export interface PgvectorOptions {
|
|
92
|
+
/** turns the fact's text into the vector the table indexes; the same function the retrieval side uses */
|
|
93
|
+
embed: (text: string) => Promise<number[]>;
|
|
94
|
+
/** the vector's length, fixed per table */
|
|
95
|
+
dimensions: number;
|
|
96
|
+
/** table name, optionally schema-qualified; default "agent_memories"; created if missing */
|
|
97
|
+
table?: string;
|
|
98
|
+
/** how many nearest rows verifyRemoved inspects for the removed id; default 10 */
|
|
99
|
+
topK?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Write-through to a pgvector table: the fact's text, its embedding, and its custody metadata as one row, keyed by
|
|
103
|
+
* the fact id, in the Postgres a team already runs. Removal deletes the row, and verification embeds the text again
|
|
104
|
+
* and checks the removed id is not among the nearest rows, which is what a retrieval query would return.
|
|
105
|
+
*/
|
|
106
|
+
export declare function pgvectorStore(client: PgLike, opts: PgvectorOptions): Store;
|
package/dist/stores.js
CHANGED
|
@@ -56,3 +56,43 @@ export function zepStore(client, opts) {
|
|
|
56
56
|
: {}),
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Write-through to a pgvector table: the fact's text, its embedding, and its custody metadata as one row, keyed by
|
|
61
|
+
* the fact id, in the Postgres a team already runs. Removal deletes the row, and verification embeds the text again
|
|
62
|
+
* and checks the removed id is not among the nearest rows, which is what a retrieval query would return.
|
|
63
|
+
*/
|
|
64
|
+
export function pgvectorStore(client, opts) {
|
|
65
|
+
const table = opts.table ?? "agent_memories";
|
|
66
|
+
if (!/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)?$/.test(table))
|
|
67
|
+
throw new Error(`pgvector: table must be a plain identifier, optionally schema-qualified; got "${table}"`);
|
|
68
|
+
if (!(Number.isInteger(opts.dimensions) && opts.dimensions > 0))
|
|
69
|
+
throw new Error("pgvector: dimensions must be a positive integer");
|
|
70
|
+
let ready = null;
|
|
71
|
+
const init = () => (ready ??= (async () => {
|
|
72
|
+
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
73
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, text TEXT NOT NULL, embedding vector(${opts.dimensions}) NOT NULL, metadata JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`);
|
|
74
|
+
})());
|
|
75
|
+
const literal = (v) => {
|
|
76
|
+
if (v.length !== opts.dimensions)
|
|
77
|
+
throw new Error(`pgvector: embedding has ${v.length} dimensions, the table has ${opts.dimensions}`);
|
|
78
|
+
return `[${v.join(",")}]`;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
name: "pgvector",
|
|
82
|
+
async put(fact) {
|
|
83
|
+
await init();
|
|
84
|
+
const text = factText(fact);
|
|
85
|
+
await client.query(`INSERT INTO ${table} (id, text, embedding, metadata) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET text = EXCLUDED.text, embedding = EXCLUDED.embedding, metadata = EXCLUDED.metadata`, [fact.factId, text, literal(await opts.embed(text)), JSON.stringify(factMetadata(fact))]);
|
|
86
|
+
return fact.factId;
|
|
87
|
+
},
|
|
88
|
+
async remove(externalId) {
|
|
89
|
+
await init();
|
|
90
|
+
await client.query(`DELETE FROM ${table} WHERE id = $1`, [externalId]);
|
|
91
|
+
},
|
|
92
|
+
async verifyRemoved(externalId, fact) {
|
|
93
|
+
await init();
|
|
94
|
+
const rows = (await client.query(`SELECT id FROM ${table} ORDER BY embedding <=> $1 LIMIT $2`, [literal(await opts.embed(factText(fact))), opts.topK ?? 10])).rows;
|
|
95
|
+
return !rows.some((r) => r.id === externalId);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-custody/state",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Governed memory for AI agents: a fact ledger with provenance, valid time, rollback, lineage, certified forget, and a store that is a JSONL file, SQLite, or Postgres, built on @agent-custody/receipts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=22"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@agent-custody/receipts": "0.5.
|
|
36
|
+
"@agent-custody/receipts": "0.5.6",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
38
38
|
"zod": "^4.5.4"
|
|
39
39
|
},
|