Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evidence.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
2a45eb148b07bec8c0acd422bd6532022aaa2ab94ea142aa2d91519bc562b869 README.md
614212892a210668c46691fc13e1c50d7f688f53b5b024da514ee8609bd5f9af README.md
4ac0e5d10b11751d8c871c482731477b1b3603abee4b81279608a34bde37978a manifest.json
79eba6766b460b5e907cc83d1db5ca61a84326806f4cb6d93b1ceb4b9ce3112f authority-effect-results.md
7f6fff7d37f14dc563914a242f0b70f8a950d6356b8f17671786d8c0a4b521c8 authority-effect-results.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
34688c283148a0524b550666fa74839b38410c26fd63425734a2753ba89f040c 6-terminal-receipt.json
b6849d19f65356cb3da0a1d0d4e840c1e1244830c9c2a637641f0cf7cf468616 health-at-run.json
d1919890e1d5ff3c5da0f552c90e09ef672e514c7f2dc3e35c91276358c926a6 verify.mjs
52c153a1a05e7e6b915e531a8b4ef81fa0322226e3df51b3d231e918195b61f5 README.md
1ec9b2d5c050eaf75ec9cf18b8d15e828d3c0efcfd07693880f3073e8b3d11f9 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion public/evidence/aaif/authority-at-dispatch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
32 changes: 32 additions & 0 deletions public/evidence/aaif/authority-at-dispatch/reproduce/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<run-id>
node aad.mjs report runs/<run-id>
```

`verify` needs no network and no account. Run one case with `node aad.mjs run <case>`; 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 |
Expand All @@ -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/<run-id> --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 |
Expand Down
102 changes: 98 additions & 4 deletions public/evidence/aaif/authority-at-dispatch/reproduce/aad.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <run-dir> offline: checksums, every envelope, every verdict
* node aad.mjs report <run-dir> 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)
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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+/);
Expand All @@ -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; }
Expand All @@ -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.`);
}
Expand All @@ -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 <setup|run [case ...]|verify <run-dir>|cases>');
console.log('usage: node aad.mjs <setup|run [case ...]|verify <run-dir> [--expect-signer 0x…]|report <run-dir>|cases>');
process.exitCode = command ? 1 : 0;
} else {
commands[command]().catch((error) => { console.error(`FAILED: ${error?.message || error}`); process.exitCode = 1; });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"verify": "node aad.mjs verify"
},
"dependencies": {
"@proofable/sdk": "0.1.6",
"@proofable/sdk": "0.1.10",
"ethers": "6.17.0"
}
}
8 changes: 8 additions & 0 deletions public/evidence/registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down
2 changes: 1 addition & 1 deletion public/evidence/registry/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
9179940f215a20477b817f0e7c9639af2d7492ebcf2d70cbce8d473e7e80b977 *README.md
bfa35912750b7102a873440a082b985d62a94b638e3a3aa7984c0741cd4e6690 *README.md
f297f8fbff06de768fe1a2ee3ecb0a00518ecbb34c4517d607d598c04f3b45c7 *agent-registration.json