@agent-custody/receipts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +269 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +111 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +66 -0
- package/dist/crypto.d.ts +42 -0
- package/dist/crypto.js +92 -0
- package/dist/delegation.d.ts +24 -0
- package/dist/delegation.js +31 -0
- package/dist/gateway.d.ts +21 -0
- package/dist/gateway.js +158 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/issue.d.ts +7 -0
- package/dist/issue.js +21 -0
- package/dist/log.d.ts +23 -0
- package/dist/log.js +109 -0
- package/dist/policy.d.ts +16 -0
- package/dist/policy.js +28 -0
- package/dist/receipt.d.ts +114 -0
- package/dist/receipt.js +11 -0
- package/dist/sdk/claude.d.ts +53 -0
- package/dist/sdk/claude.js +52 -0
- package/dist/sdk/index.d.ts +41 -0
- package/dist/sdk/index.js +77 -0
- package/dist/sdk/langchain.d.ts +17 -0
- package/dist/sdk/langchain.js +61 -0
- package/dist/sdk/openai-agents.d.ts +11 -0
- package/dist/sdk/openai-agents.js +66 -0
- package/dist/sdk/vercel-ai.d.ts +7 -0
- package/dist/sdk/vercel-ai.js +35 -0
- package/dist/verify.d.ts +22 -0
- package/dist/verify.js +107 -0
- package/docs/policies.md +138 -0
- package/docs/sdk.md +177 -0
- package/docs/tutorials.md +35 -0
- package/docs/usage.md +166 -0
- package/docs/verification.md +139 -0
- package/package.json +87 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Verifying a receipt
|
|
2
|
+
|
|
3
|
+
A verifier needs three things and no network access:
|
|
4
|
+
|
|
5
|
+
1. the receipt bundle, one JSON file
|
|
6
|
+
2. the issuer's public key: the gateway key, or the application key for SDK receipts
|
|
7
|
+
3. the principal's public key, for gateway receipts, which carry a signed delegation
|
|
8
|
+
|
|
9
|
+
A fourth is optional: a copy of the issuer's log file, which lets the verifier confirm the receipt sits in a log whose root the verifier recomputed, not one the issuer merely asserted.
|
|
10
|
+
|
|
11
|
+
## From the command line
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node src/cli.ts verify receipts/<id>.json \
|
|
15
|
+
--issuer-key keys/gateway.pub \
|
|
16
|
+
--principal-key keys/principal.pub \
|
|
17
|
+
--log log.jsonl # optional
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Exit code 0 when every check passes, 1 otherwise. Add `--json` for machine-readable output. `--issuer-key` and `--principal-key` can be repeated to accept rotated keys; signatures are matched by keyid. `--gateway-key` is an alias for `--issuer-key`. `--principal-key` may be omitted for SDK receipts, which carry no delegation.
|
|
21
|
+
|
|
22
|
+
A real report, produced by `node scripts/demo.ts`:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
PASS receipt signature (issuer key) (keyid f2f53b689a84)
|
|
26
|
+
PASS receipt payload type
|
|
27
|
+
PASS issuer kind is known (gateway)
|
|
28
|
+
PASS issuer keyid matches signer
|
|
29
|
+
PASS gateway receipt carries a delegation
|
|
30
|
+
PASS gateway receipt carries a policy decision
|
|
31
|
+
PASS delegation signature (principal key) (signed by bfa1a505f55b)
|
|
32
|
+
PASS delegation binds principal and agent
|
|
33
|
+
PASS delegation valid at receipt time (2026-09-04T13:06:20.074Z .. 2026-09-04T14:06:21.074Z)
|
|
34
|
+
PASS executed tool within delegated scope (stripe.refund)
|
|
35
|
+
PASS request args digest
|
|
36
|
+
PASS policy decision consistent with execution (allow -> executed)
|
|
37
|
+
PASS no policy errors on an allow
|
|
38
|
+
PASS tree head signature
|
|
39
|
+
PASS tree head matches inclusion proof size
|
|
40
|
+
PASS log inclusion proof (leaf 0 of 1, root 4efaea879930)
|
|
41
|
+
PASS log file root matches tree head (recomputed 4efaea879930)
|
|
42
|
+
|
|
43
|
+
RESULT: VERIFIED
|
|
44
|
+
|
|
45
|
+
ISSUER: gateway, enforced outside the agent's process; the agent could neither skip nor forge this receipt
|
|
46
|
+
|
|
47
|
+
field provenance value
|
|
48
|
+
principal attested user_456
|
|
49
|
+
agent attested support-agent
|
|
50
|
+
model claimed claude-fable-5-1
|
|
51
|
+
tool observed stripe.refund
|
|
52
|
+
args claimed {"amount":50000,"customer_id":"cust_123"}
|
|
53
|
+
fact.customer observed {"email":"alex@example.com","id":"cust_123","verified":true}
|
|
54
|
+
policy observed allow [policy1] policy ba4e4461ffc4
|
|
55
|
+
execution observed executed
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What each check means
|
|
59
|
+
|
|
60
|
+
| check | what it establishes | what a failure usually means |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| receipt signature (issuer key) | the bundle's envelope was signed by a key you trust as an issuer and has not changed since | edited receipt, or an issuer key you do not trust |
|
|
63
|
+
| receipt payload type | the payload is an in-toto Statement with this project's predicate type | wrong file, or a different envelope replayed as a receipt |
|
|
64
|
+
| issuer kind is known | the receipt says whether a gateway or an SDK produced it; the detail shows which, and the framework | a predicate this verifier does not understand |
|
|
65
|
+
| issuer keyid matches signer | the receipt's own claim of who signed it matches the actual signature | mixed-up or forged predicate |
|
|
66
|
+
| gateway receipt carries a delegation | gateway receipts always embed the signed grant they enforced | an SDK receipt relabelled as gateway |
|
|
67
|
+
| gateway receipt carries a policy decision | gateway receipts always record the Cedar decision | same |
|
|
68
|
+
| delegation signature (principal key) | the embedded grant was signed by a key you trust as a principal | a grant the principal never issued |
|
|
69
|
+
| delegation binds principal and agent | the grant names the same principal and agent the receipt names, and the principal keyid matches | a valid grant for someone else, spliced in |
|
|
70
|
+
| delegation valid at receipt time | the receipt's timestamp is inside the grant's window | expired or not-yet-valid authority |
|
|
71
|
+
| executed tool within delegated scope | if the tool ran, the grant covered it. Denied calls pass this check by construction | a gateway that forwarded out of scope |
|
|
72
|
+
| principal is claimed, not attested | SDK receipts only: there is no delegation, so the principal is a config string and is labelled as such | an SDK receipt pretending to an attested principal |
|
|
73
|
+
| request args digest | the args in the predicate hash to the digest in the subject | edited arguments |
|
|
74
|
+
| policy decision consistent with execution | allow went with executed or failed; deny went with denied. Skipped when no policy was evaluated | an issuer that executed after a deny |
|
|
75
|
+
| no policy errors on an allow | an allow was not produced while Cedar reported errors | broken fail-closed behaviour |
|
|
76
|
+
| tree head signature | the tree head was signed by a trusted issuer key | forged log position |
|
|
77
|
+
| tree head matches inclusion proof size | the proof and the tree head describe the same tree | mismatched bundle parts |
|
|
78
|
+
| log inclusion proof | this exact envelope is a leaf of the tree with that root | receipt never logged, or logged then changed |
|
|
79
|
+
| log file root matches tree head | recomputing the root from your copy of the log at that size gives the same value | your log copy and the issuer's history diverge: deletion, reordering, or edit |
|
|
80
|
+
|
|
81
|
+
Gateway receipts run seventeen checks, eighteen with a log file. SDK receipts run fewer, because there is no delegation to check, and the report says so on the `principal is claimed` line.
|
|
82
|
+
|
|
83
|
+
## What a verified receipt lets you conclude
|
|
84
|
+
|
|
85
|
+
Read the `ISSUER` line first, then the provenance column. For the executed refund above, a gateway receipt, a verified result supports exactly this statement:
|
|
86
|
+
|
|
87
|
+
> The gateway holding key `f2f5…` observed that the agent `support-agent`, acting under a grant signed by the holder of principal key `bfa1…` for `user_456`, requested `stripe.refund` with these arguments; the gateway itself looked up the customer and got `verified: true`; policy with digest `ba4e…` allowed it; the upstream server returned a result with this digest; and the gateway committed all of that to position 0 of a log whose root is `4efa…`.
|
|
88
|
+
|
|
89
|
+
For an SDK receipt the statement is shorter: a process holding the application key reported this call, with these arguments and this result, and the record has not changed since. Nothing in it was checked outside that process. The report prints this under `ISSUER` so nobody has to remember it.
|
|
90
|
+
|
|
91
|
+
A gateway receipt does **not** support:
|
|
92
|
+
|
|
93
|
+
- that Stripe really executed the refund. The upstream result is `observed`, not signed by Stripe. That is the first roadmap item.
|
|
94
|
+
- that the arguments were correct. They are `claimed`: they are what the agent asked for, which is what a receipt should record.
|
|
95
|
+
- that the model named in `model` produced the call. No hosted provider signs model identity.
|
|
96
|
+
- that the gateway operator is honest. The operator holds the gateway key. Against a dishonest operator you need a log copy taken out of their control, or a signer they do not control. See the threat model table in the README.
|
|
97
|
+
|
|
98
|
+
## Programmatic verification
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { readFileSync } from "node:fs";
|
|
102
|
+
import { loadPublicKey } from "../src/crypto.ts";
|
|
103
|
+
import { verifyBundle, formatReport } from "../src/verify.ts";
|
|
104
|
+
|
|
105
|
+
const bundle = JSON.parse(readFileSync("receipts/<id>.json", "utf8"));
|
|
106
|
+
const result = verifyBundle(bundle, {
|
|
107
|
+
issuerKeys: [loadPublicKey("keys/gateway.pub")], // gateway keys and SDK application keys
|
|
108
|
+
principalKeys: [loadPublicKey("keys/principal.pub")],
|
|
109
|
+
logFile: "log.jsonl", // optional
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
result.ok // every check passed
|
|
113
|
+
result.checks // [{ name, ok, detail? }]
|
|
114
|
+
result.statement?.predicate // the decoded receipt, only when the signature verified
|
|
115
|
+
console.log(formatReport(result));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`verifyBundle` is pure and synchronous. It reads the log file only when `logFile` is given.
|
|
119
|
+
|
|
120
|
+
## Verifying without this codebase
|
|
121
|
+
|
|
122
|
+
The formats are standard on purpose, so a verifier in another language needs no code from here:
|
|
123
|
+
|
|
124
|
+
- **Envelope:** [DSSE](https://github.com/secure-systems-lab/dsse). Signature is Ed25519 over `"DSSEv1 " + len(payloadType) + " " + payloadType + " " + len(payload) + " " + payload`.
|
|
125
|
+
- **Payload:** [in-toto Statement v1](https://github.com/in-toto/attestation), canonical JSON with sorted keys and no whitespace.
|
|
126
|
+
- **keyid:** sha256 of the SPKI DER encoding of the public key, hex.
|
|
127
|
+
- **Log:** RFC 6962 hashing (`0x00` prefix for leaves, `0x01` for nodes) and the RFC 9162 inclusion-proof algorithm. Leaves are the canonical JSON of the envelope. The log file is one JSON string per line.
|
|
128
|
+
|
|
129
|
+
## Auditing a log copy
|
|
130
|
+
|
|
131
|
+
Take copies of `log.jsonl` on a schedule and keep them where the operator cannot write. Then for any receipt:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node src/cli.ts verify receipts/<id>.json --issuer-key ... --principal-key ... --log /audit/copies/log-2026-09-04.jsonl
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The last check recomputes the root at the receipt's tree size from your copy. If the operator later deletes, reorders, or edits a line before that position, the recomputed root changes and the check fails.
|
|
138
|
+
|
|
139
|
+
Today a copy must be at least as long as the receipt's tree size. Consistency proofs between two tree heads, which would let you check that a newer log extends an older copy without holding the whole file, are on the roadmap.
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-custody/receipts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chain of custody for AI agents: signed, independently verifiable receipts for tool calls. MCP gateway + Cedar policy + Merkle transparency log",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ch4r10t33r/agent-custody.git",
|
|
9
|
+
"directory": "packages/receipts"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"agent-custody": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./sdk": {
|
|
24
|
+
"types": "./dist/sdk/index.d.ts",
|
|
25
|
+
"default": "./dist/sdk/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./sdk/claude": {
|
|
28
|
+
"types": "./dist/sdk/claude.d.ts",
|
|
29
|
+
"default": "./dist/sdk/claude.js"
|
|
30
|
+
},
|
|
31
|
+
"./sdk/openai-agents": {
|
|
32
|
+
"types": "./dist/sdk/openai-agents.d.ts",
|
|
33
|
+
"default": "./dist/sdk/openai-agents.js"
|
|
34
|
+
},
|
|
35
|
+
"./sdk/vercel-ai": {
|
|
36
|
+
"types": "./dist/sdk/vercel-ai.d.ts",
|
|
37
|
+
"default": "./dist/sdk/vercel-ai.js"
|
|
38
|
+
},
|
|
39
|
+
"./sdk/langchain": {
|
|
40
|
+
"types": "./dist/sdk/langchain.d.ts",
|
|
41
|
+
"default": "./dist/sdk/langchain.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"docs"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"demo": "tsx scripts/demo.ts"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@cedar-policy/cedar-wasm": "^4.12.0",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
60
|
+
"zod": "^4.5.4"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@langchain/core": "^1.2.9",
|
|
64
|
+
"@openai/agents": "^0.17.0",
|
|
65
|
+
"@types/node": "^26.4.1",
|
|
66
|
+
"ai": "^7.0.92",
|
|
67
|
+
"tsx": "^4.23.13",
|
|
68
|
+
"typescript": "^7.0.2",
|
|
69
|
+
"vitest": "^5.0.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"@langchain/core": ">=1.0.0",
|
|
73
|
+
"@openai/agents": ">=0.1.0",
|
|
74
|
+
"ai": ">=5.0.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@langchain/core": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"@openai/agents": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"ai": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|