diff --git a/CHANGELOG.md b/CHANGELOG.md index ad49fe9..66c534e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@agnt-rcpt/sdk-ts` to `^0.11.1` (superseding the `^0.10.0` dependabot bump). As of 0.11.0 the SDK dropped its transitive `@hpke/core` dependency in favour of an in-tree RFC 9180 implementation built on `node:crypto` (per [agent-receipts/ar#473](https://github.com/agent-receipts/ar/issues/473)), removing a third-party crypto package from the plugin's supply chain — `^0.10.0` still resolved `@hpke/core` transitively, `^0.11.1` does not. The `parameters_disclosure` envelope shape and the on-the-wire format are unchanged. + +### Added + +- `ar_query_receipts` results now include a `disclosed` boolean, set when the daemon attached an HPKE `parameters_disclosure` envelope (its `--parameter-disclosure` mode). The plugin never decrypts the envelope — recovery requires the forensic private key, which lives with the responder, not the agent host. + ### Fixed - **Socket emission silent failures and misleading startup warnings** ([#148](https://github.com/agent-receipts/openclaw/issues/148)): diff --git a/package.json b/package.json index df18169..51b122e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@agnt-rcpt/sdk-ts": "^0.10.0", + "@agnt-rcpt/sdk-ts": "^0.11.1", "@sinclair/typebox": "0.34.49" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3276f2..190e037 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@agnt-rcpt/sdk-ts': - specifier: ^0.10.0 - version: 0.10.0 + specifier: ^0.11.1 + version: 0.11.1 '@sinclair/typebox': specifier: 0.34.49 version: 0.34.49 @@ -38,8 +38,8 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - '@agnt-rcpt/sdk-ts@0.10.0': - resolution: {integrity: sha512-LiUAIqhe9zmeQjVeoQoGmRA3oZXHKr7J8/bxx8GOtk+Q6oBuESnVn23DzuaIWNmeb3Kx/te3MY1wAe7DtlfYtw==} + '@agnt-rcpt/sdk-ts@0.11.1': + resolution: {integrity: sha512-rJ6qx5phkTw2sGbAPQSL8WOpU7lXk19p7voSzuQaC6oZSzy13/IW7eJznlDJ0ayi9AOgAg07gaaGuN8BqhJvRA==} engines: {node: '>=22.11.0'} '@anthropic-ai/sdk@0.91.1': @@ -405,14 +405,6 @@ packages: peerDependencies: hono: ^4 - '@hpke/common@1.10.1': - resolution: {integrity: sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==} - engines: {node: '>=16.0.0'} - - '@hpke/core@1.9.0': - resolution: {integrity: sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==} - engines: {node: '>=16.0.0'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -482,30 +474,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@mariozechner/clipboard-linux-arm64-musl@0.3.6': resolution: {integrity: sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@mariozechner/clipboard-linux-riscv64-gnu@0.3.6': resolution: {integrity: sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@mariozechner/clipboard-linux-x64-gnu@0.3.6': resolution: {integrity: sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@mariozechner/clipboard-linux-x64-musl@0.3.6': resolution: {integrity: sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@mariozechner/clipboard-win32-arm64-msvc@0.3.6': resolution: {integrity: sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==} @@ -569,30 +566,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.100': resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.100': resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.100': resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/canvas-win32-arm64-msvc@0.1.100': resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} @@ -687,66 +689,79 @@ packages: resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} @@ -2206,9 +2221,8 @@ snapshots: dependencies: zod: 4.4.3 - '@agnt-rcpt/sdk-ts@0.10.0': + '@agnt-rcpt/sdk-ts@0.11.1': dependencies: - '@hpke/core': 1.9.0 undici: 8.3.0 zod: 4.4.3 @@ -2669,12 +2683,6 @@ snapshots: dependencies: hono: 4.12.23 - '@hpke/common@1.10.1': {} - - '@hpke/core@1.9.0': - dependencies: - '@hpke/common': 1.10.1 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 diff --git a/src/tools.test.ts b/src/tools.test.ts index 7e52a10..0ab0b86 100644 --- a/src/tools.test.ts +++ b/src/tools.test.ts @@ -2,22 +2,27 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { randomUUID } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { openStore, generateKeyPair, createReceipt, signReceipt, hashReceipt, + encryptDisclosure, + decryptDisclosure, + generateForensicKeyPair, type ReceiptStore, type RiskLevel, type OutcomeStatus, + type DisclosureEnvelope, } from "@agnt-rcpt/sdk-ts"; import { createQueryReceiptsTool, createVerifyChainTool, createVerifyChainToolFactory, } from "./tools.js"; +import { openDaemonStore } from "./daemon-store.js"; // ---- Test fixture helpers ---- @@ -37,6 +42,7 @@ function insertReceiptAt( actionType?: string; riskLevel?: RiskLevel; status?: OutcomeStatus; + disclosure?: DisclosureEnvelope; }, ): string { const unsigned = createReceipt({ @@ -47,6 +53,7 @@ function insertReceiptAt( risk_level: opts.riskLevel ?? "low", target: { system: "openclaw", resource: "read_file" }, parameters_hash: "abc123", + ...(opts.disclosure ? { parameters_disclosure: opts.disclosure } : {}), }, outcome: { status: opts.status ?? "success", @@ -65,6 +72,15 @@ function insertReceiptAt( return h; } +/** + * Forensic-key fingerprint: "sha256:" + lowercase hex SHA-256 of the raw 32-byte + * X25519 public key — the same convention the daemon uses to derive a recipient + * `kid` (see the parameter-disclosure spec). + */ +function forensicKid(publicKey: Uint8Array): string { + return `sha256:${createHash("sha256").update(publicKey).digest("hex")}`; +} + // ---- ar_query_receipts ---- describe("ar_query_receipts", () => { @@ -508,3 +524,150 @@ describe("ar_verify_chain", () => { expect(data.valid).toBe(true); }); }); + +// ---- HPKE parameter disclosure: cross-engine read path ---- +// +// The daemon (Go) WRITES the HPKE `parameters_disclosure` envelope into the +// SQLite DB; the plugin READS it back through the TS SDK's strict zod schema +// (which `store.query()` / `verifyStoredChain()` run on every load). These +// tests pin that an envelope of the shape the daemon emits is accepted on the +// read path, that the Ed25519 signature commits to it, and that the ciphertext +// stays recoverable — but only with the forensic private key, which lives with +// the responder, not here. The plugin itself never decrypts. + +describe("HPKE parameter disclosure — cross-engine read path", () => { + let tempDir: string; + let dbPath: string; + let pubKeyPath: string; + let keys: ReturnType; + let forensic: Awaited>; + let kid: string; + let writableStore: ReceiptStore; + + beforeEach(async () => { + tempDir = join(tmpdir(), `ar-test-${randomUUID()}`); + mkdirSync(tempDir, { recursive: true }); + dbPath = join(tempDir, "receipts.db"); + pubKeyPath = join(tempDir, "signing.key.pub"); + keys = generateKeyPair(); + writeFileSync(pubKeyPath, keys.publicKey); + forensic = await generateForensicKeyPair(); + kid = forensicKid(forensic.publicKey); + writableStore = openStore(dbPath); + }); + + afterEach(() => { + writableStore.close(); + rmSync(tempDir, { recursive: true, force: true }); + }); + + // Insert a receipt whose action carries an HPKE disclosure envelope encrypted + // to the forensic public key — the shape the daemon writes when its + // --parameter-disclosure mode fires. Returns the stored receipt hash. + async function insertDisclosedReceipt(opts: { + seq: number; + chainId: string; + timestamp: string; + previousHash: string | null; + params: Record; + }): Promise { + const envelope = await encryptDisclosure(opts.params, forensic.publicKey, kid); + return insertReceiptAt(writableStore, keys.privateKey, { + seq: opts.seq, + chainId: opts.chainId, + timestamp: opts.timestamp, + previousHash: opts.previousHash, + actionType: "system.command.execute", + riskLevel: "high", + disclosure: envelope, + }); + } + + it("flags disclosed receipts in ar_query_receipts and accepts the daemon envelope shape", async () => { + await insertDisclosedReceipt({ + seq: 1, + chainId: "chain-d", + timestamp: "2024-01-01T10:00:00.000Z", + previousHash: null, + params: { command: "rm -rf /tmp/x", cwd: "/home/me" }, + }); + insertReceiptAt(writableStore, keys.privateKey, { + seq: 2, + chainId: "chain-d", + timestamp: "2024-01-01T11:00:00.000Z", + previousHash: "h1", + actionType: "filesystem.file.read", + }); + + const tool = createQueryReceiptsTool({ daemonDbPath: dbPath, daemonPublicKeyPath: pubKeyPath }); + const result = await tool.execute("tc-q", {}); + const data = JSON.parse(result.content[0].text); + + const disclosedByAction = Object.fromEntries( + data.results.map((r: { action: string; disclosed: boolean }) => [r.action, r.disclosed]), + ); + expect(disclosedByAction["system.command.execute"]).toBe(true); + expect(disclosedByAction["filesystem.file.read"]).toBe(false); + }); + + it("verifies a chain whose receipts carry HPKE disclosure envelopes", async () => { + const h1 = await insertDisclosedReceipt({ + seq: 1, + chainId: "chain-d", + timestamp: "2024-01-01T10:00:00.000Z", + previousHash: null, + params: { command: "echo hi" }, + }); + await insertDisclosedReceipt({ + seq: 2, + chainId: "chain-d", + timestamp: "2024-01-01T11:00:00.000Z", + previousHash: h1, + params: { command: "cat secret" }, + }); + + const tool = createVerifyChainTool({ daemonDbPath: dbPath, daemonPublicKeyPath: pubKeyPath }); + const result = await tool.execute("tc-v", { chain_id: "chain-d" }); + + const data = JSON.parse(result.content[1].text); + expect(data.valid).toBe(true); + expect(data.length).toBe(2); + for (const r of data.receipts) { + expect(r.signature_valid).toBe(true); + expect(r.hash_link_valid).toBe(true); + expect(r.sequence_valid).toBe(true); + } + }); + + it("keeps the disclosed parameters recoverable only with the forensic private key", async () => { + const params = { command: "rm -rf /tmp/x", cwd: "/home/me" }; + await insertDisclosedReceipt({ + seq: 1, + chainId: "chain-d", + timestamp: "2024-01-01T10:00:00.000Z", + previousHash: null, + params, + }); + + const store = openDaemonStore(dbPath); + try { + const [receipt] = store.query({}); + expect(receipt).toBeDefined(); + const envelope = receipt.credentialSubject.action.parameters_disclosure; + if (!envelope) throw new Error("expected a disclosure envelope on the stored receipt"); + // The stored envelope carries the forensic key fingerprint a responder uses + // to locate matching receipts. + expect(envelope.recipients[0].kid).toBe(kid); + // The forensic key holder (off-host) recovers the plaintext; the plugin + // never performs this step. + const recovered = await decryptDisclosure(envelope, forensic.privateKey); + expect(recovered).toEqual(params); + // Exclusivity: an unrelated forensic key cannot recover the parameters — + // the AEAD tag fails to authenticate, so decryption rejects. + const otherForensic = await generateForensicKeyPair(); + await expect(decryptDisclosure(envelope, otherForensic.privateKey)).rejects.toThrow(); + } finally { + store.close(); + } + }); +}); diff --git a/src/tools.ts b/src/tools.ts index cb1712b..aaab14b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -148,6 +148,12 @@ export function createQueryReceiptsToolFactory(deps: ToolDeps) { status: r.credentialSubject.outcome.status, sequence: r.credentialSubject.chain.sequence, timestamp: r.credentialSubject.action.timestamp, + // True when the daemon attached an HPKE parameters_disclosure + // envelope (its --parameter-disclosure mode). The plugin never + // decrypts it — recovery needs the forensic private key, which + // lives with the responder, not the agent host. This flag only + // signals that the parameters are recoverable by that key holder. + disclosed: r.credentialSubject.action.parameters_disclosure !== undefined, })), };