diff --git a/evidence.mdx b/evidence.mdx index fec4978..2062e6a 100644 --- a/evidence.mdx +++ b/evidence.mdx @@ -88,7 +88,7 @@ Live web pages change between requests and cause unpredictable token spikes when **8 of 8 permission tests passed on the live service.** Every action outside the agent's permission was stopped before it ran, and the actions it was allowed to take ran. -We ran each test on the live service on October 8, 2026, with a dedicated agent under its own fixed permission, and again on October 24 on the revision production serves ([records](https://github.com/proofable/docs/tree/main/public/evidence/aaif/authority-at-dispatch/2026-10-24-served)), 8 of 8 both times. Each link opens that test's signed record. +We ran each test on the live service on October 8, 2026, with a dedicated agent under its own fixed permission, and again on October 24 on the revision production was serving then ([records](https://github.com/proofable/docs/tree/main/public/evidence/aaif/authority-at-dispatch/2026-10-24-served)), 8 of 8 both times. Each link opens that test's signed record. | Test | What happened | |---|---| diff --git a/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/README.md b/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/README.md index 0d3bf8f..8ce5015 100644 --- a/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/README.md +++ b/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/README.md @@ -66,9 +66,13 @@ your own Proofable account, so the records it writes are yours, not Proofable's. `SHA256SUMS` covers every payload file in this directory except itself. Verify with `sha256sum -c SHA256SUMS`. -Install the pinned `@proofable/sdk@0.1.6` package once. Then verify every signature and qHash offline: +Install `@proofable/sdk@0.1.10` (or later) once, then verify every signature and qHash offline: `node verify-portable-proofs.mjs` +Use 0.1.9 or later. Earlier versions require a non-canonical `walletAddress` field and cannot verify a +CAIP-380 envelope that omits it; 0.1.9 and later read the signer from the envelope's `did`. This packet's +envelopes verify under 0.1.10, checked offline. + Disclosure controls which fields of the Proofable record are shared; non-public fields are not required to evaluate these results. diff --git a/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/SHA256SUMS b/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/SHA256SUMS index db2bc99..00992cb 100644 --- a/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/SHA256SUMS +++ b/public/evidence/aaif/authority-at-dispatch/2026-10-08-eight-case/SHA256SUMS @@ -1,4 +1,4 @@ -2a45eb148b07bec8c0acd422bd6532022aaa2ab94ea142aa2d91519bc562b869 README.md +614212892a210668c46691fc13e1c50d7f688f53b5b024da514ee8609bd5f9af README.md 4ac0e5d10b11751d8c871c482731477b1b3603abee4b81279608a34bde37978a manifest.json 79eba6766b460b5e907cc83d1db5ca61a84326806f4cb6d93b1ceb4b9ce3112f authority-effect-results.md 7f6fff7d37f14dc563914a242f0b70f8a950d6356b8f17671786d8c0a4b521c8 authority-effect-results.json diff --git a/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/README.md b/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/README.md index 45bee46..09c750b 100644 --- a/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/README.md +++ b/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/README.md @@ -10,6 +10,27 @@ This answers the request in [agentrust-io/integrations#333](https://github.com/agentrust-io/integrations/issues/333) for a copyable example of the dispatch-only, correlation-only route. +## What this establishes, and what it does not + +Classified against AAIF Identity & Trust [#21](https://github.com/aaif/wg-identity-and-trust/issues/21) +and [agentrust-io/integrations#333](https://github.com/agentrust-io/integrations/issues/333), the +defensible statement for this packet is **a service-attested job decision with correlation links**. +Prior authorisation is not established by it. Three links are missing: + +1. **The grant is named by commitment only.** `delegationQHash` cannot be resolved to the mandate + being evaluated, so its bounds cannot be checked from this packet. +2. **Nothing binds the mandate digest and version to what the counterparty received.** The + decision-to-job tie is an unsigned create response, and the target kept no log. The full marker + first appears in the signed terminal receipt, after the call. +3. **The ordering rests on the deciding service's signed timestamp.** A token taken later dates the + record; it does not witness the dispatch. + +What it does establish: the decision is signed before the job exists, it commits to the grant's +`delegationQHash` and to an `argsDigest` over a declared field projection of the request, and the +terminal receipt is signed by the same service account. That account is published outside this +packet, in the pinned registration document whose bytes and registry read are at +[`public/evidence/registry`](https://github.com/proofable/docs/blob/f81b8416a4f6fc1f1439ef43216078261fb206d2/public/evidence/registry/README.md). + ## Files | File | What it is | Signed | diff --git a/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/SHA256SUMS b/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/SHA256SUMS index 0b741f9..f530594 100644 --- a/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/SHA256SUMS +++ b/public/evidence/aaif/authority-at-dispatch/2026-10-24-fetch-route/SHA256SUMS @@ -6,4 +6,4 @@ 34688c283148a0524b550666fa74839b38410c26fd63425734a2753ba89f040c 6-terminal-receipt.json b6849d19f65356cb3da0a1d0d4e840c1e1244830c9c2a637641f0cf7cf468616 health-at-run.json d1919890e1d5ff3c5da0f552c90e09ef672e514c7f2dc3e35c91276358c926a6 verify.mjs -52c153a1a05e7e6b915e531a8b4ef81fa0322226e3df51b3d231e918195b61f5 README.md +1ec9b2d5c050eaf75ec9cf18b8d15e828d3c0efcfd07693880f3073e8b3d11f9 README.md diff --git a/public/evidence/aaif/authority-at-dispatch/2026-10-24-served/README.md b/public/evidence/aaif/authority-at-dispatch/2026-10-24-served/README.md index 27c342b..406bae6 100644 --- a/public/evidence/aaif/authority-at-dispatch/2026-10-24-served/README.md +++ b/public/evidence/aaif/authority-at-dispatch/2026-10-24-served/README.md @@ -8,8 +8,9 @@ The code that graded it is `reproduce/aad.mjs` at SHA-256 `afbb0cf8541c3916967707143b5be56556051b32797b2a6b52aae6f423234f79`. Check that version out to grade the run with the exact code that graded it. -This is the current run. It replaces [`../2026-10-08-eight-case/`](../2026-10-08-eight-case/), which -stays at its own commit. +This run replaces [`../2026-10-08-eight-case/`](../2026-10-08-eight-case/), which stays at its own +commit. Both are dated runs on the revision each was taken against; the service moved on afterwards, so +read the revision here as what production served on 24 October, not as what it serves now. ## What happened diff --git a/public/evidence/aaif/authority-at-dispatch/README.md b/public/evidence/aaif/authority-at-dispatch/README.md index 4c4233b..a4b4739 100644 --- a/public/evidence/aaif/authority-at-dispatch/README.md +++ b/public/evidence/aaif/authority-at-dispatch/README.md @@ -6,7 +6,7 @@ link to every record, is at | Folder | What it is | | --- | --- | -| [`2026-10-24-served/`](./2026-10-24-served/) | **Current run.** Eight cases on revision `61724c9f4`, 17 signed records, checksums, and a result for each case | +| [`2026-10-24-served/`](./2026-10-24-served/) | **Latest published run, 24 October.** Eight cases on revision `61724c9f4`, 17 signed records, checksums, and a result for each case | | [`2026-10-24-fetch-route/`](./2026-10-24-fetch-route/) | One job on the fetch route: the call, the raw response, the signed decision and receipt, and an offline checker | | [`reproduce/`](./reproduce/) | Run the same cases under your own account | | [`action/`](./action/) | The same run as a GitHub Action, which writes a `trustharness.report/v1` report | diff --git a/public/evidence/aaif/authority-at-dispatch/reproduce/README.md b/public/evidence/aaif/authority-at-dispatch/reproduce/README.md index 1702b07..fadf5a8 100644 --- a/public/evidence/aaif/authority-at-dispatch/reproduce/README.md +++ b/public/evidence/aaif/authority-at-dispatch/reproduce/README.md @@ -22,11 +22,16 @@ export AAD_OPERATOR="Your Organization" node aad.mjs setup # creates a local agent wallet and registers your review agent node aad.mjs run # all eight cases, about 6 to 8 minutes node aad.mjs verify runs/ +node aad.mjs report runs/ ``` `verify` needs no network and no account. Run one case with `node aad.mjs run `; list them with `node aad.mjs cases`. +Verify with `@proofable/sdk` 0.1.9 or later, which this kit pins. Earlier versions require a +non-canonical `walletAddress` field on the envelope, so they cannot verify a CAIP-380 envelope that +omits it, including the CAIP's own test vectors. + ## The cases | Case | What it does | Expected | @@ -45,6 +50,33 @@ with `node aad.mjs cases`. How long a revocation takes to reach the check, and the effect at the target: effects are recorded as the service observed them. +## Typed outcomes, typed cost and the grading scope + +`report` reads a run offline and prints one row per case: the separate outcome fields (what was +attempted, what was observed, what the service accepted, whether effects were verified) each with a +typed reason where the case refused or skipped, the benign cost of the check and the legitimate work +it must not forbid, and a statement of what the run does not grade. It writes nothing. + +- `elapsed_ms` is measured on the kit's clock, over the whole case including setup and waits. +- `job_ms` is measured by the service inside the signed job record, over the executed part of the job, + and is `not_applicable` where no job ran. +- Model tokens are `not_measured`: the kit does not count them. +- `effects_verified` is `not_measured` for every case. Effects are observed by the service, with no + target-side witness, and `MANIFEST.json` carries the same entry. + +## Pin the signer + +`verify` recovers each signature against the address declared inside that same envelope. To also +require the address you read out of band: + +```sh +node aad.mjs verify runs/ --expect-signer 0xa41494e1e5f9e5eaf786961e6ef84cd1a3071eec +``` + +That address is the receipt signer named in the registration document published at +[`docs/public/evidence/registry`](https://github.com/proofable/docs/tree/main/public/evidence/registry), +together with the registry read that binds the document to Proofable's on-chain agent identity. + ## Settings | Variable | Default | Purpose | diff --git a/public/evidence/aaif/authority-at-dispatch/reproduce/aad.mjs b/public/evidence/aaif/authority-at-dispatch/reproduce/aad.mjs index e135161..673a8a3 100644 --- a/public/evidence/aaif/authority-at-dispatch/reproduce/aad.mjs +++ b/public/evidence/aaif/authority-at-dispatch/reproduce/aad.mjs @@ -9,8 +9,14 @@ * node aad.mjs setup register your review agent and its delegation * node aad.mjs run [case ...] run all cases (default) or the named ones * node aad.mjs verify offline: checksums, every envelope, every verdict + * node aad.mjs report offline: per-case typed outcomes, typed cost, grading scope * node aad.mjs cases list the cases * + * verify also takes `--expect-signer 0x…`, the address you read out of band (for the published + * packets, the receipt signer named in the pinned registration document at docs/public/evidence/ + * registry). With it, a record whose declared wallet is not that address fails rather than passing + * on its own envelope's word. + * * Environment (see README.md): * PROOFABLE_ACCESS_KEY npk_… access key (required) * AAD_OPERATOR who ran this, recorded as custodian (required for setup/run) @@ -415,6 +421,16 @@ function envelopesOf(rec) { for (const a of rec.replayAttempts || []) add(a.authorityDecision); return out; } +/** The signed record bodies inside a case's envelopes, parsed. Used by report only. */ +function signedContents(rec) { + const out = []; + for (const e of envelopesOf(rec)) { + const raw = e?.data?.content; + if (typeof raw !== 'string' || !raw.startsWith('{')) continue; + try { out.push(JSON.parse(raw)); } catch { /* not a record body */ } + } + return out; +} // ── run ───────────────────────────────────────────────────────────────────────────────── function requireOperator() { @@ -487,9 +503,11 @@ function seal(dir, identity) { } // ── verify: offline, from the files alone ─────────────────────────────────────────────── -async function verify(dirArg) { +async function verify(dirArg, options = {}) { const dir = resolve(String(dirArg || '')); if (!existsSync(join(dir, 'SHA256SUMS'))) throw new Error('Pass a run directory that contains SHA256SUMS.'); + const expectSigner = options.expectSigner ? lower(String(options.expectSigner).trim()) : ''; + if (expectSigner && !/^0x[0-9a-f]{40}$/.test(expectSigner)) throw new Error('--expect-signer needs a 0x address.'); let failures = 0; for (const line of readFileSync(join(dir, 'SHA256SUMS'), 'utf8').split('\n').filter(Boolean)) { const [digest, name] = line.split(/\s+/); @@ -498,6 +516,7 @@ async function verify(dirArg) { const manifest = JSON.parse(readFileSync(join(dir, 'MANIFEST.json'), 'utf8')); const owner = lower(manifest.delegationRoot.split(':').pop()); const rows = []; + let signerFailures = 0; for (const name of CASE_ORDER) { const file = join(dir, `${name}.json`); if (!existsSync(file)) { rows.push([name, 'MISSING', '-']); failures += 1; continue; } @@ -507,12 +526,14 @@ async function verify(dirArg) { for (const e of envelopes) { const v = await verifyPortableProofEnvelope(e); if (!v.valid || lower(e.data?.owner) !== owner) bad += 1; + else if (expectSigner && lower(e.walletAddress) !== expectSigner) { bad += 1; signerFailures += 1; } } const ok = verdict(rec) && bad === 0 && envelopes.length > 0; if (!ok) failures += 1; rows.push([name, ok ? 'PASS' : 'FAIL', `${envelopes.length - bad}/${envelopes.length} envelopes`]); } console.log(`run ${manifest.run_id}, revision ${manifest.product_revision}, custody ${manifest.custody}`); + if (expectSigner) console.log(`signer pin ${expectSigner}: ${signerFailures ? `${signerFailures} record(s) signed by another address` : 'every record is signed by it'}`); if (manifest.verifier_sha256 && manifest.verifier_sha256 !== VERIFIER_SHA256) { console.log(`graded by aad.mjs ${manifest.verifier_sha256.slice(0, 12)}; this copy is ${VERIFIER_SHA256.slice(0, 12)}. Check out the grading version to repeat the verdict exactly.`); } @@ -521,16 +542,89 @@ async function verify(dirArg) { process.exitCode = failures ? 1 : 0; } +// ── report: typed outcomes, typed cost and the grading scope, read offline ────────────── +const msBetween = (from, to) => { + const a = Date.parse(from); const b = Date.parse(to); + return Number.isFinite(a) && Number.isFinite(b) && b >= a ? b - a : null; +}; +function report(dirArg) { + const dir = resolve(String(dirArg || '')); + if (!existsSync(join(dir, 'MANIFEST.json'))) throw new Error('Pass a run directory that contains MANIFEST.json.'); + const manifest = JSON.parse(readFileSync(join(dir, 'MANIFEST.json'), 'utf8')); + console.log(`run ${manifest.run_id}, revision ${manifest.product_revision}, custody ${manifest.custody}`); + console.log('grading scope: the authority decision in each case, and nothing beyond it. Every case hands the'); + console.log('service a sentence rather than a rule a machine can check, so the job\'s own acceptance flag is'); + console.log('recorded and not graded.'); + console.log(''); + const rows = []; + for (const name of CASE_ORDER) { + const file = join(dir, `${name}.json`); + const row = { case: name, attempted: 'no', dispatch: 'record missing', observed: 'not_emitted', accepted: 'not_emitted', effects: 'not_measured', elapsed_ms: null, job_ms: null, permitted: 'unknown' }; + if (existsSync(file)) { + const rec = JSON.parse(readFileSync(file, 'utf8')); + const bodies = signedContents(rec); + const decisions = bodies.filter((b) => b.kind === 'agent-job-authority-decision'); + const job = bodies.find((b) => b.kind === 'agent-job'); + const evaluation = job?.evaluation || {}; + Object.assign(row, { + attempted: decisions.length || rec.jobId ? 'yes' : 'no', + dispatch: decisions.map((d) => `${d.outcome}${d.reason ? `: ${d.reason}` : ''}`).join(' | ') || 'none', + observed: job ? `job:${lower(job.status) || 'unknown'}` : 'no job', + accepted: Object.hasOwn(evaluation, 'accepted') ? String(evaluation.accepted) : 'not_emitted', + effects: 'not_measured', + elapsed_ms: msBetween(rec.started, rec.finished), + job_ms: typeof job?.durationMs === 'number' ? job.durationMs : null, + permitted: rec.control + ? `the valid control request ran and ${lower(rec.control.status) || 'was recorded'}` + : (job ? (lower(job.status) === 'completed' ? 'the permitted request ran and completed' : `the permitted request did not complete (${lower(job.status)})`) : 'not applicable'), + detail: rec.observed_effect ? `${rec.observed_effect.evidence}, committed ${rec.observed_effect.committed}` : '', + }); + } + rows.push(row); + } + const head = ['case', 'attempted', 'observed', 'accepted', 'elapsed_ms', 'job_ms']; + const widths = [24, 10, 14, 12, 11, 8]; + console.log(head.map((h, i) => h.padEnd(widths[i])).join('')); + for (const r of rows) { + console.log([ + r.case.padEnd(widths[0]), r.attempted.padEnd(widths[1]), r.observed.padEnd(widths[2]), + r.accepted.padEnd(widths[3]), + String(r.elapsed_ms ?? 'not_measured').padEnd(widths[4]), String(r.job_ms ?? 'not_applicable').padEnd(widths[5]), + ].join('')); + } + console.log(''); + console.log('Per case, what the authority decided, how far the action got, and the legitimate work:'); + for (const r of rows) { + console.log(` ${r.case}`); + console.log(` dispatch: ${r.dispatch}`); + console.log(` effect: ${r.detail || 'no separate effect record'}`); + console.log(` permitted: ${r.permitted}`); + } + console.log(''); + console.log('Benign cost, typed. elapsed_ms is measured on this kit\'s clock, scope the whole case including'); + console.log('setup and waits. job_ms is measured by the service inside the signed job record, scope the'); + console.log('executed portion of the job. Model tokens are not_measured: the kit does not count them.'); + console.log(''); + console.log('effects_verified is not_measured for every case: effects are observed by the service, with no'); + console.log('target-side witness (the same entry as not_measured in MANIFEST.json). Read this report from the'); + console.log('sealed records only; it writes nothing.'); +} + // ── entry ─────────────────────────────────────────────────────────────────────────────── const [command, ...rest] = process.argv.slice(2); +const flag = (name) => { + const i = rest.indexOf(name); + return i === -1 ? undefined : rest[i + 1]; +}; const commands = { setup: () => setup(), - run: () => run(rest), - verify: () => verify(rest[0]), + run: () => run(rest.filter((a) => !a.startsWith('--'))), + verify: () => verify(rest[0], { expectSigner: flag('--expect-signer') }), + report: async () => report(rest[0]), cases: async () => console.log(CASE_ORDER.join('\n')), }; if (!commands[command]) { - console.log('usage: node aad.mjs |cases>'); + console.log('usage: node aad.mjs [--expect-signer 0x…]|report |cases>'); process.exitCode = command ? 1 : 0; } else { commands[command]().catch((error) => { console.error(`FAILED: ${error?.message || error}`); process.exitCode = 1; }); diff --git a/public/evidence/aaif/authority-at-dispatch/reproduce/package.json b/public/evidence/aaif/authority-at-dispatch/reproduce/package.json index a69a37f..443d7f6 100644 --- a/public/evidence/aaif/authority-at-dispatch/reproduce/package.json +++ b/public/evidence/aaif/authority-at-dispatch/reproduce/package.json @@ -13,7 +13,7 @@ "verify": "node aad.mjs verify" }, "dependencies": { - "@proofable/sdk": "0.1.6", + "@proofable/sdk": "0.1.10", "ethers": "6.17.0" } } diff --git a/public/evidence/registry/README.md b/public/evidence/registry/README.md index 32a4e3a..17b8f74 100644 --- a/public/evidence/registry/README.md +++ b/public/evidence/registry/README.md @@ -7,6 +7,14 @@ This exists so the document can be read, hashed and quoted from a fixed commit without depending on a live HTTP request to proofable.me. +Pinned bytes: [`agent-registration.json`](https://github.com/proofable/docs/blob/f81b8416a4f6fc1f1439ef43216078261fb206d2/public/evidence/registry/agent-registration.json) +at commit `f81b841`, and the same file served at +`https://docs.proofable.me/public/evidence/registry/agent-registration.json`. + +Re-read 2026-11-01: the document served at the `.well-known` URL is byte-identical to the pinned copy +above, same SHA-256. The registry read below was repeated the same day against the head block +(`53381986`) on two public Base RPC endpoints, with the same result. + ## The registry binding The document is the target of Proofable's on-chain agent registration. Read on Base (`eip155:8453`), diff --git a/public/evidence/registry/SHA256SUMS b/public/evidence/registry/SHA256SUMS index 3f3448b..9320c28 100644 --- a/public/evidence/registry/SHA256SUMS +++ b/public/evidence/registry/SHA256SUMS @@ -1,2 +1,2 @@ -9179940f215a20477b817f0e7c9639af2d7492ebcf2d70cbce8d473e7e80b977 *README.md +bfa35912750b7102a873440a082b985d62a94b638e3a3aa7984c0741cd4e6690 *README.md f297f8fbff06de768fe1a2ee3ecb0a00518ecbb34c4517d607d598c04f3b45c7 *agent-registration.json