@agent-custody/receipts 0.5.9 → 0.6.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/README.md +6 -5
- package/dist/cli.js +38 -5
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -2
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +4 -0
- package/dist/delegation.d.ts +34 -1
- package/dist/delegation.js +89 -5
- package/dist/gateway-http.d.ts +30 -0
- package/dist/gateway-http.js +139 -0
- package/dist/gateway.d.ts +14 -0
- package/dist/gateway.js +142 -113
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/verify.js +6 -1
- package/docs/tutorials.md +1 -0
- package/docs/usage.md +6 -0
- package/docs/verification.md +2 -1
- package/package.json +2 -2
- package/vectors/audit.json +27 -27
- package/vectors/canonical.json +5 -5
- package/vectors/receipts.json +318 -216
package/dist/gateway.js
CHANGED
|
@@ -57,24 +57,16 @@ function extractValue(result) {
|
|
|
57
57
|
return text.text;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
export async function
|
|
60
|
+
export async function createGatewayHost(cfg, options = {}) {
|
|
61
61
|
const gatewayKey = loadPrivateKey(cfg.identity.keyFile);
|
|
62
62
|
const trusted = cfg.trustedPrincipalKeys.map(loadPublicKey);
|
|
63
|
-
const grantEnvelope = JSON.parse(readFileSync(cfg.grantFile, "utf8"));
|
|
64
|
-
const grant = verifyDelegation(grantEnvelope, trusted);
|
|
65
|
-
if (!grant.ok)
|
|
66
|
-
throw new Error(`delegation grant rejected: ${grant.error}`);
|
|
67
|
-
if (!delegationValidAt(grant.delegation, new Date().toISOString()))
|
|
68
|
-
throw new Error("delegation grant is outside its validity window");
|
|
69
|
-
const delegation = grant.delegation;
|
|
70
|
-
const principalKeyid = grant.keyid;
|
|
71
63
|
const policyText = readFileSync(cfg.policyFile, "utf8");
|
|
72
64
|
const pDigest = policyDigest(policyText);
|
|
73
65
|
const issuer = createIssuer(gatewayKey, cfg.receiptsDir, options.log ?? openLog(cfg, gatewayKey), { exporter: options.exporter ?? openExporter(cfg) });
|
|
74
66
|
const precommit = new Set(cfg.precommit);
|
|
75
67
|
const consequential = (tool) => precommit.has("*") || precommit.has(tool);
|
|
76
|
-
// One
|
|
77
|
-
//
|
|
68
|
+
// One host, as many upstreams as the agents' jobs need. Each tool name belongs to exactly one upstream, decided at
|
|
69
|
+
// startup, so a receipt's tool is unambiguous and consumed facts flow across them.
|
|
78
70
|
const upstreamConfigs = cfg.upstreams ? cfg.upstreams.map((u) => ({ name: u.name, cfg: u })) : [{ name: "upstream", cfg: cfg.upstream }];
|
|
79
71
|
const upstreams = new Map();
|
|
80
72
|
const owner = new Map();
|
|
@@ -128,124 +120,161 @@ export async function createGateway(cfg, options = {}) {
|
|
|
128
120
|
}
|
|
129
121
|
return facts;
|
|
130
122
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
context: { args, facts: factValues, grant: { principal: delegation.principal, scopes: delegation.scopes } },
|
|
165
|
-
});
|
|
123
|
+
function open(grantEnvelope) {
|
|
124
|
+
const grant = verifyDelegation(grantEnvelope, trusted);
|
|
125
|
+
if (!grant.ok)
|
|
126
|
+
throw new Error(`delegation grant rejected: ${grant.error}`);
|
|
127
|
+
if (!delegationValidAt(grant.delegation, new Date().toISOString()))
|
|
128
|
+
throw new Error("delegation grant is outside its validity window");
|
|
129
|
+
const delegation = grant.delegation;
|
|
130
|
+
const principalKeyid = grant.keyid;
|
|
131
|
+
/** Every fact id an upstream has declared it served to this session, in order of first sight. A session is one agent under one grant. */
|
|
132
|
+
const consumed = [];
|
|
133
|
+
const noteServedFacts = (result) => {
|
|
134
|
+
const ids = result._meta?.[FACTS_META_KEY];
|
|
135
|
+
if (!Array.isArray(ids))
|
|
136
|
+
return;
|
|
137
|
+
for (const id of ids)
|
|
138
|
+
if (typeof id === "string" && !consumed.includes(id))
|
|
139
|
+
consumed.push(id);
|
|
140
|
+
};
|
|
141
|
+
async function handleCall(params) {
|
|
142
|
+
const tool = params.name;
|
|
143
|
+
const args = params.arguments ?? {};
|
|
144
|
+
const receiptId = randomUUID();
|
|
145
|
+
const timestamp = new Date().toISOString();
|
|
146
|
+
const modelClaim = params._meta?.[MODEL_META_KEY];
|
|
147
|
+
// What the agent had been shown before this call; recorded before this call's own result is seen.
|
|
148
|
+
const consumedNow = [...consumed];
|
|
149
|
+
const upstreamMeta = { [RECEIPT_META_KEY]: receiptId, [AGENT_META_KEY]: delegation.agent, [PRINCIPAL_META_KEY]: delegation.principal };
|
|
150
|
+
let facts = {};
|
|
151
|
+
let policy;
|
|
152
|
+
let execution;
|
|
153
|
+
let authorization;
|
|
154
|
+
if (!delegation.scopes.includes(tool)) {
|
|
155
|
+
policy = { decision: "deny", reasons: [], errors: [`tool "${tool}" is not in the delegation scopes`], policyDigest: pDigest };
|
|
166
156
|
}
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
else {
|
|
158
|
+
try {
|
|
159
|
+
facts = await gatherFacts(tool, args, upstreamMeta);
|
|
160
|
+
const factValues = Object.fromEntries(Object.entries(facts).map(([k, f]) => [k, f.value]));
|
|
161
|
+
policy = evaluate(policyText, {
|
|
162
|
+
agentId: delegation.agent,
|
|
163
|
+
tool,
|
|
164
|
+
context: { args, facts: factValues, grant: { principal: delegation.principal, scopes: delegation.scopes } },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
policy = { decision: "deny", reasons: [], errors: [String(e instanceof Error ? e.message : e)], policyDigest: pDigest };
|
|
169
|
+
}
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
171
|
+
const head = {
|
|
172
|
+
receiptId,
|
|
173
|
+
timestamp,
|
|
174
|
+
issuer: { kind: "gateway", keyid: issuer.keyid, version: GATEWAY_VERSION },
|
|
175
|
+
principal: { id: delegation.principal, keyid: principalKeyid, provenance: "attested" },
|
|
176
|
+
agent: { id: delegation.agent, provenance: "attested" },
|
|
177
|
+
delegation: { envelope: grantEnvelope, provenance: "attested" },
|
|
178
|
+
tool: { name: tool, provenance: "observed", ...(owner.has(tool) && upstreamConfigs.length > 1 ? { upstream: owner.get(tool) } : {}) },
|
|
179
|
+
request: { args, argsDigest: digestOf(args), provenance: "claimed" },
|
|
180
|
+
facts,
|
|
181
|
+
consumed: { factIds: consumedNow, provenance: "observed" },
|
|
182
|
+
};
|
|
183
|
+
if (policy.decision === "allow" && consequential(tool)) {
|
|
184
|
+
// A consequential call is committed to the log before it goes out, so that evidence of the side effect exists
|
|
185
|
+
// before the side effect does. If the log will not take the authorization, the call is not forwarded.
|
|
186
|
+
try {
|
|
187
|
+
authorization = await issuer.authorize({ ...head, policy: { ...policy, provenance: "observed" } });
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
execution = { status: "withheld", reason: `the log did not commit the authorization, so the call was not forwarded: ${String(e instanceof Error ? e.message : e)}`, provenance: "observed" };
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
if (execution) {
|
|
194
|
+
// withheld: nothing was forwarded
|
|
191
195
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
else if (policy.decision === "allow") {
|
|
197
|
+
try {
|
|
198
|
+
// The upstream learns which receipt this call is, and who the grant says is calling. An upstream that keeps
|
|
199
|
+
// state, such as the memory server, cites the receipt as the source of what it stores.
|
|
200
|
+
const observed = Object.fromEntries(Object.entries(facts).map(([k, f]) => [k, f.value]));
|
|
201
|
+
const result = await callUpstream(tool, args, { ...upstreamMeta, [OBSERVED_META_KEY]: observed });
|
|
202
|
+
const evidence = upstreamEvidenceOf(result);
|
|
203
|
+
execution = { status: result.isError ? "failed" : "executed", result, resultDigest: digestOf(result), provenance: "observed", ...(evidence ? { upstream: evidence } : {}) };
|
|
204
|
+
noteServedFacts(result);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
execution = { status: "error", error: String(e instanceof Error ? e.message : e), provenance: "observed" };
|
|
208
|
+
}
|
|
205
209
|
}
|
|
206
|
-
|
|
207
|
-
execution = { status: "
|
|
210
|
+
else {
|
|
211
|
+
execution = { status: "denied", reason: [...policy.reasons, ...policy.errors].join("; ") || "no permit policy matched", provenance: "observed" };
|
|
208
212
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
execution
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
default: {
|
|
231
|
-
const result = execution.result;
|
|
232
|
-
return { ...result, _meta: { ...result._meta, ...meta } };
|
|
213
|
+
await issuer.issue({
|
|
214
|
+
...head,
|
|
215
|
+
session: { id: null, toolUseId: null, provenance: "claimed" },
|
|
216
|
+
model: { id: typeof modelClaim === "string" ? modelClaim : null, provenance: "claimed" },
|
|
217
|
+
policy: { ...policy, provenance: "observed" },
|
|
218
|
+
...(authorization ? { authorization } : {}),
|
|
219
|
+
execution,
|
|
220
|
+
});
|
|
221
|
+
const meta = { [RECEIPT_META_KEY]: receiptId };
|
|
222
|
+
const refuse = (text) => ({ isError: true, content: [{ type: "text", text: `${text} (receipt ${receiptId})` }], _meta: meta });
|
|
223
|
+
switch (execution.status) {
|
|
224
|
+
case "denied":
|
|
225
|
+
return refuse(`Denied by policy: ${execution.reason}`);
|
|
226
|
+
case "error":
|
|
227
|
+
return refuse(`Upstream error: ${execution.error}`);
|
|
228
|
+
case "withheld":
|
|
229
|
+
return refuse(`Not executed: ${execution.reason}`);
|
|
230
|
+
default: {
|
|
231
|
+
const result = execution.result;
|
|
232
|
+
return { ...result, _meta: { ...result._meta, ...meta } };
|
|
233
|
+
}
|
|
233
234
|
}
|
|
234
235
|
}
|
|
236
|
+
return {
|
|
237
|
+
agentId: delegation.agent,
|
|
238
|
+
delegation,
|
|
239
|
+
async listTools() {
|
|
240
|
+
return advertised.filter((t) => delegation.scopes.includes(t.name));
|
|
241
|
+
},
|
|
242
|
+
handleCall,
|
|
243
|
+
async close() {
|
|
244
|
+
// a session holds nothing of its own beyond what it consumed; the host owns the upstreams
|
|
245
|
+
},
|
|
246
|
+
};
|
|
235
247
|
}
|
|
236
248
|
return {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
async listTools() {
|
|
240
|
-
return advertised.filter((t) => delegation.scopes.includes(t.name));
|
|
241
|
-
},
|
|
242
|
-
handleCall,
|
|
249
|
+
keyid: issuer.keyid,
|
|
250
|
+
open,
|
|
243
251
|
async close() {
|
|
244
252
|
for (const c of upstreams.values())
|
|
245
253
|
await c.close();
|
|
246
254
|
},
|
|
247
255
|
};
|
|
248
256
|
}
|
|
257
|
+
/** One gateway for the grant the config names: what `agent-custody gateway` serves over stdio. Closing it closes the host. */
|
|
258
|
+
export async function createGateway(cfg, options = {}) {
|
|
259
|
+
if (!cfg.grantFile)
|
|
260
|
+
throw new Error("config needs grantFile for a single-grant gateway; over HTTP each connection presents its own grant");
|
|
261
|
+
const host = await createGatewayHost(cfg, options);
|
|
262
|
+
let session;
|
|
263
|
+
try {
|
|
264
|
+
session = host.open(JSON.parse(readFileSync(cfg.grantFile, "utf8")));
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
await host.close();
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
...session,
|
|
272
|
+
async close() {
|
|
273
|
+
await session.close();
|
|
274
|
+
await host.close();
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
249
278
|
/** Exposes the gateway as an MCP server over stdio. Everything diagnostic must go to stderr. */
|
|
250
279
|
export async function serveStdio(gw) {
|
|
251
280
|
const server = new Server({ name: "agent-custody-gateway", version: GATEWAY_VERSION }, { capabilities: { tools: {} } });
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { GatewayOptions } from "./gateway.ts";
|
|
|
4
4
|
export { buildRequest, restUpstream } from "./rest.ts";
|
|
5
5
|
export { openExporter, otlpExporter, spanFor } from "./otel.ts";
|
|
6
6
|
export { hecEvent, splunkExporter } from "./splunk.ts";
|
|
7
|
+
export { GRANT_HEADER, grantHeader, parseGrantHeader, serveHttp } from "./gateway-http.ts";
|
|
8
|
+
export type { HttpGatewayOptions, RunningHttpGateway } from "./gateway-http.ts";
|
|
7
9
|
export { exportLog, formatExport } from "./log-export.ts";
|
|
8
10
|
export type { ExportOptions, ExportResult } from "./log-export.ts";
|
|
9
11
|
export { fileBackend, importLogFile, PostgresLog, PostgresTenancy, RateLimiter } from "./log-store.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { AUTHORIZATION_PREDICATE_TYPE, buildAuthorizationStatement } from "./rec
|
|
|
3
3
|
export { buildRequest, restUpstream } from "./rest.js";
|
|
4
4
|
export { openExporter, otlpExporter, spanFor } from "./otel.js";
|
|
5
5
|
export { hecEvent, splunkExporter } from "./splunk.js";
|
|
6
|
+
export { GRANT_HEADER, grantHeader, parseGrantHeader, serveHttp } from "./gateway-http.js";
|
|
6
7
|
export { exportLog, formatExport } from "./log-export.js";
|
|
7
8
|
export { fileBackend, importLogFile, PostgresLog, PostgresTenancy, RateLimiter } from "./log-store.js";
|
|
8
9
|
export { connectSigner, fetchLogKeys, localSigner, serveSigner, signerHandler } from "./signer.js";
|
package/dist/verify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Independent verification of a receipt bundle. Needs only public keys, and optionally a copy of the log.
|
|
2
2
|
import { canonicalize, digestOf, dsseVerifiers, dsseVerify } from "./crypto.js";
|
|
3
|
-
import { delegationValidAt, verifyDelegation } from "./delegation.js";
|
|
3
|
+
import { decodeDelegation, delegationValidAt, describeChain, verifyDelegation } from "./delegation.js";
|
|
4
4
|
import { leafHash, MerkleLog, verifyConsistency, verifyInclusion } from "./log.js";
|
|
5
5
|
import { checkProvider, checkUpstream, contentDigest, isProviderAttestation } from "./upstream.js";
|
|
6
6
|
import { AUTHORIZATION_PREDICATE_TYPE, RECEIPT_PREDICATE_TYPE, RECEIPT_TYPE, TREEHEAD_TYPE } from "./receipt.js";
|
|
@@ -31,6 +31,11 @@ export function verifyBundle(bundle, opts) {
|
|
|
31
31
|
if (p.delegation) {
|
|
32
32
|
const del = verifyDelegation(p.delegation.envelope, opts.principalKeys);
|
|
33
33
|
add("delegation signature (principal key)", del.ok, del.ok ? `signed by ${short(del.keyid)}` : del.error);
|
|
34
|
+
// A chained grant: every link signed by the key its parent names, scopes and windows nested, one principal
|
|
35
|
+
// throughout. The line appears only when the grant embeds a parent, and fails with the link that broke.
|
|
36
|
+
const chained = decodeDelegation(p.delegation.envelope)?.parent !== undefined;
|
|
37
|
+
if (chained)
|
|
38
|
+
add("delegation chain to the principal", del.ok, del.ok ? `${describeChain(del.chain)} (${del.chain.length - 1} delegation(s))` : del.error);
|
|
34
39
|
if (del.ok) {
|
|
35
40
|
const d = del.delegation;
|
|
36
41
|
const principalKeyid = p.principal.provenance === "attested" ? p.principal.keyid : null;
|
package/docs/tutorials.md
CHANGED
|
@@ -29,6 +29,7 @@ Suggested reading order is the numbering. Output lands in `examples-out/`, which
|
|
|
29
29
|
| 17 | a REST API as an upstream | [17-rest-upstream.ts](../examples/17-rest-upstream.ts) | a stand-in payments API described as two tools, the token from the environment, a refund allowed on the gateway's own lookup and one denied before reaching the API, the receipt verified | `src/rest.ts`, `src/gateway.ts` |
|
|
30
30
|
| 18 | OpenTelemetry export | [18-opentelemetry.ts](../examples/18-opentelemetry.ts) | a stand-in OTLP collector, `otel` in the config, one span per receipt with the receipt id as trace id, the collector going away and the next receipt still issued | `src/otel.ts`, `src/issue.ts` |
|
|
31
31
|
| 19 | Splunk export | [19-splunk.ts](../examples/19-splunk.ts) | a stand-in HTTP Event Collector, `splunk` in the config with the token from the environment, one event per receipt with the receipt id and log position as fields, the collector going away and the next receipt still issued | `src/splunk.ts`, `src/otel.ts` |
|
|
32
|
+
| 20 | one gateway for many agents, over HTTP | [20-http-gateway.ts](../examples/20-http-gateway.ts) | a gateway host served over Streamable HTTP, two agents connecting with their own grants and seeing their own tools, receipts naming the right agent, a refusal by grant, and a stranger's grant getting no session | `src/gateway-http.ts`, `src/gateway.ts` |
|
|
32
33
|
|
|
33
34
|
## How policies are defined, in one paragraph
|
|
34
35
|
|
package/docs/usage.md
CHANGED
|
@@ -42,6 +42,8 @@ node src/cli.ts grant \
|
|
|
42
42
|
|
|
43
43
|
The gateway refuses to start if the grant is outside its validity window, and every receipt records the grant so a verifier can re-check it.
|
|
44
44
|
|
|
45
|
+
**Sub-agents.** A grant that names the agent's own public key (`--agent-key agent.pub`, carried as `agentKey`) lets that agent delegate: `agent-custody delegate --key agent.key --parent grant.json --agent refunder --scopes stripe.refund --out refunder.json` signs a narrower grant for the sub-agent with the parent grant embedded. The chain may go three delegations deep. A verifier walks it back to the principal: every link must be signed by the key its parent names, every scope must be one the parent holds, every window must sit inside the parent's, and the principal never changes; the gateway applies the same rule before opening a session, and the receipt names the sub-agent as the agent and the principal as the principal, with the whole chain inside, so `verify` reports `delegation chain to the principal: user_456 → planner → refunder`. Nothing about the tools changes: the sub-agent sees the scopes its own grant names and no more.
|
|
46
|
+
|
|
45
47
|
**3. Write a policy.** A Cedar file. Default is deny. See [policies.md](policies.md).
|
|
46
48
|
|
|
47
49
|
```cedar
|
|
@@ -123,6 +125,10 @@ node src/cli.ts gateway --config ./gateway.json
|
|
|
123
125
|
|
|
124
126
|
You will not normally run this by hand. The agent host spawns it, as below.
|
|
125
127
|
|
|
128
|
+
## One gateway for many agents, over HTTP
|
|
129
|
+
|
|
130
|
+
`agent-custody gateway --config gateway.json --http --port 8790` serves the same gateway as an MCP server over Streamable HTTP at `/mcp`, and every connection presents its own grant: the delegation envelope, base64url-encoded, as `Authorization: Bearer <value>` or `X-Agent-Custody-Grant` on the initialize request. The gateway verifies it against `trustedPrincipalKeys` and its validity window, and opens a session for exactly that grant; a grant signed by a stranger, an expired one, or none at all gets 403 with the reason and no session (403 rather than 401, because MCP clients treat 401 as an OAuth challenge and hide the body). Sessions share the key, the policy, the upstreams, the log, and the fact lookups; each has its own tools (the scopes its grant names), its own receipts (its own principal and agent, attested), and its own consumed facts. `grantFile` in the config is then optional and ignored. A session ends when the client terminates it or after `--idle-minutes` (default 30) without a request; `GET /health` reports the live count and the gateway's key id. The transport is plain HTTP: bind to loopback, a private network, or put TLS in front. From JavaScript, `grantHeader(envelope)` builds the header value; from any language it is `base64url(JSON.stringify(envelope))`. Tutorial 20 runs two agents against one gateway.
|
|
131
|
+
|
|
126
132
|
## Wiring it into an agent host
|
|
127
133
|
|
|
128
134
|
The gateway is an ordinary MCP server, so any host that can launch a stdio MCP server can use it. Point the host at the gateway instead of at the upstream server.
|
package/docs/verification.md
CHANGED
|
@@ -70,6 +70,7 @@ execution observed executed
|
|
|
70
70
|
| gateway receipt carries a delegation | gateway receipts always embed the signed grant they enforced | an SDK receipt relabelled as gateway |
|
|
71
71
|
| gateway receipt carries a policy decision | gateway receipts always record the Cedar decision | same |
|
|
72
72
|
| delegation signature (principal key) | the embedded grant was signed by a key you trust as a principal | a grant the principal never issued |
|
|
73
|
+
| delegation chain to the principal | only when the grant embeds a parent: every link is signed by the key its parent names, holds no scope its parent lacks, sits inside its parent's window, and names the same principal, up to three delegations deep | a sub-agent given more than its delegator had, a link signed by the wrong key, a chain that never reaches a trusted principal |
|
|
73
74
|
| 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 |
|
|
74
75
|
| delegation valid at receipt time | the receipt's timestamp is inside the grant's window | expired or not-yet-valid authority |
|
|
75
76
|
| 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 |
|
|
@@ -88,7 +89,7 @@ execution observed executed
|
|
|
88
89
|
| log inclusion proof | this exact envelope is a leaf of the tree with that root | receipt never logged, or logged then changed |
|
|
89
90
|
| 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 |
|
|
90
91
|
|
|
91
|
-
Gateway receipts run seventeen checks, eighteen with a log file, and five more when the tool was committed before it ran (`precommit` in the gateway config). SDK receipts run fewer, because there is no delegation to check, and the report says so on the `principal is claimed` line.
|
|
92
|
+
Gateway receipts run seventeen checks, eighteen with a log file, and five more when the tool was committed before it ran (`precommit` in the gateway config). SDK receipts run fewer, because there is no delegation to check, and the report says so on the `principal is claimed` line. A receipt issued to a sub-agent under a delegation chain runs one more, `delegation chain to the principal`.
|
|
92
93
|
|
|
93
94
|
## What a verified receipt lets you conclude
|
|
94
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-custody/receipts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Chain of custody for AI agents: signed, independently verifiable receipts for tool calls. MCP gateway + Cedar policy + Merkle transparency log",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@langchain/core": "^1.2.9",
|
|
70
|
-
"@openai/agents": "^0.
|
|
70
|
+
"@openai/agents": "^0.18.0",
|
|
71
71
|
"@types/node": "^26.4.1",
|
|
72
72
|
"ai": "^7.0.92",
|
|
73
73
|
"tsx": "^4.23.13",
|
package/vectors/audit.json
CHANGED
|
@@ -7,26 +7,26 @@
|
|
|
7
7
|
"description": "The tree head from the first remote receipt and the log's later head, with the proof the log served.",
|
|
8
8
|
"older": {
|
|
9
9
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
10
|
-
"payload": "
|
|
10
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiNjk4ZjdhMDg4ZWQ3ZGMzNDhiZmNkNDBmZDhhZDUxOGQ3YzMxMjc5MTFhMDYzZjgzOTYxOGY5ZTIzNzg3OTkwMCIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTE2WiIsInRyZWVTaXplIjoxfQ==",
|
|
11
11
|
"signatures": [
|
|
12
12
|
{
|
|
13
|
-
"keyid": "
|
|
14
|
-
"sig": "
|
|
13
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
14
|
+
"sig": "tgXaLpauD+TMD8l9+qZCk4dVOlp+bhlwzNYQ/LBN3o9cjQSpeUH2Z1Qriog4jHz3HndJ3eSheBmumin04XrPAw=="
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
18
|
"newer": {
|
|
19
19
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
20
|
-
"payload": "
|
|
20
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiZTk3YzI5MjBmYTM4MGJiNTVhZjI4ODczZGNhNmY4NzM0ZGFiZTNmYjNmYjZlOWEwYmQ5YTgwNTVkNjJkYzRmYyIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTIxWiIsInRyZWVTaXplIjoyfQ==",
|
|
21
21
|
"signatures": [
|
|
22
22
|
{
|
|
23
|
-
"keyid": "
|
|
24
|
-
"sig": "
|
|
23
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
24
|
+
"sig": "D1sCDTur/vV9Agzys9RNyRQjC/z4xJU+Ch1rWRksCx8ISP54+EEL9PlDEKZrAigX6hSim1zai42ssgaR+SdGBQ=="
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
28
|
"proof": [
|
|
29
|
-
"
|
|
29
|
+
"00be3e432c1d9e0b24bdd55891e75611b4cc4da06736c2805b27bae2c1e39578"
|
|
30
30
|
],
|
|
31
31
|
"keys": [
|
|
32
32
|
"log"
|
|
@@ -41,26 +41,26 @@
|
|
|
41
41
|
"description": "The same heads the wrong way round.",
|
|
42
42
|
"older": {
|
|
43
43
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
44
|
-
"payload": "
|
|
44
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiZTk3YzI5MjBmYTM4MGJiNTVhZjI4ODczZGNhNmY4NzM0ZGFiZTNmYjNmYjZlOWEwYmQ5YTgwNTVkNjJkYzRmYyIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTIxWiIsInRyZWVTaXplIjoyfQ==",
|
|
45
45
|
"signatures": [
|
|
46
46
|
{
|
|
47
|
-
"keyid": "
|
|
48
|
-
"sig": "
|
|
47
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
48
|
+
"sig": "D1sCDTur/vV9Agzys9RNyRQjC/z4xJU+Ch1rWRksCx8ISP54+EEL9PlDEKZrAigX6hSim1zai42ssgaR+SdGBQ=="
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
52
|
"newer": {
|
|
53
53
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
54
|
-
"payload": "
|
|
54
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiNjk4ZjdhMDg4ZWQ3ZGMzNDhiZmNkNDBmZDhhZDUxOGQ3YzMxMjc5MTFhMDYzZjgzOTYxOGY5ZTIzNzg3OTkwMCIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTE2WiIsInRyZWVTaXplIjoxfQ==",
|
|
55
55
|
"signatures": [
|
|
56
56
|
{
|
|
57
|
-
"keyid": "
|
|
58
|
-
"sig": "
|
|
57
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
58
|
+
"sig": "tgXaLpauD+TMD8l9+qZCk4dVOlp+bhlwzNYQ/LBN3o9cjQSpeUH2Z1Qriog4jHz3HndJ3eSheBmumin04XrPAw=="
|
|
59
59
|
}
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
62
|
"proof": [
|
|
63
|
-
"
|
|
63
|
+
"00be3e432c1d9e0b24bdd55891e75611b4cc4da06736c2805b27bae2c1e39578"
|
|
64
64
|
],
|
|
65
65
|
"keys": [
|
|
66
66
|
"log"
|
|
@@ -77,26 +77,26 @@
|
|
|
77
77
|
"description": "Tree heads checked against the app key, which did not sign them.",
|
|
78
78
|
"older": {
|
|
79
79
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
80
|
-
"payload": "
|
|
80
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiNjk4ZjdhMDg4ZWQ3ZGMzNDhiZmNkNDBmZDhhZDUxOGQ3YzMxMjc5MTFhMDYzZjgzOTYxOGY5ZTIzNzg3OTkwMCIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTE2WiIsInRyZWVTaXplIjoxfQ==",
|
|
81
81
|
"signatures": [
|
|
82
82
|
{
|
|
83
|
-
"keyid": "
|
|
84
|
-
"sig": "
|
|
83
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
84
|
+
"sig": "tgXaLpauD+TMD8l9+qZCk4dVOlp+bhlwzNYQ/LBN3o9cjQSpeUH2Z1Qriog4jHz3HndJ3eSheBmumin04XrPAw=="
|
|
85
85
|
}
|
|
86
86
|
]
|
|
87
87
|
},
|
|
88
88
|
"newer": {
|
|
89
89
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
90
|
-
"payload": "
|
|
90
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiZTk3YzI5MjBmYTM4MGJiNTVhZjI4ODczZGNhNmY4NzM0ZGFiZTNmYjNmYjZlOWEwYmQ5YTgwNTVkNjJkYzRmYyIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTIxWiIsInRyZWVTaXplIjoyfQ==",
|
|
91
91
|
"signatures": [
|
|
92
92
|
{
|
|
93
|
-
"keyid": "
|
|
94
|
-
"sig": "
|
|
93
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
94
|
+
"sig": "D1sCDTur/vV9Agzys9RNyRQjC/z4xJU+Ch1rWRksCx8ISP54+EEL9PlDEKZrAigX6hSim1zai42ssgaR+SdGBQ=="
|
|
95
95
|
}
|
|
96
96
|
]
|
|
97
97
|
},
|
|
98
98
|
"proof": [
|
|
99
|
-
"
|
|
99
|
+
"00be3e432c1d9e0b24bdd55891e75611b4cc4da06736c2805b27bae2c1e39578"
|
|
100
100
|
],
|
|
101
101
|
"keys": [
|
|
102
102
|
"app"
|
|
@@ -114,21 +114,21 @@
|
|
|
114
114
|
"description": "A proof with a hash removed.",
|
|
115
115
|
"older": {
|
|
116
116
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
117
|
-
"payload": "
|
|
117
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiNjk4ZjdhMDg4ZWQ3ZGMzNDhiZmNkNDBmZDhhZDUxOGQ3YzMxMjc5MTFhMDYzZjgzOTYxOGY5ZTIzNzg3OTkwMCIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTE2WiIsInRyZWVTaXplIjoxfQ==",
|
|
118
118
|
"signatures": [
|
|
119
119
|
{
|
|
120
|
-
"keyid": "
|
|
121
|
-
"sig": "
|
|
120
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
121
|
+
"sig": "tgXaLpauD+TMD8l9+qZCk4dVOlp+bhlwzNYQ/LBN3o9cjQSpeUH2Z1Qriog4jHz3HndJ3eSheBmumin04XrPAw=="
|
|
122
122
|
}
|
|
123
123
|
]
|
|
124
124
|
},
|
|
125
125
|
"newer": {
|
|
126
126
|
"payloadType": "application/vnd.agent-custody.treehead+json",
|
|
127
|
-
"payload": "
|
|
127
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiZTk3YzI5MjBmYTM4MGJiNTVhZjI4ODczZGNhNmY4NzM0ZGFiZTNmYjNmYjZlOWEwYmQ5YTgwNTVkNjJkYzRmYyIsInRpbWVzdGFtcCI6IjIwMjYtMDktMjFUMDk6Mjc6MzIuMTIxWiIsInRyZWVTaXplIjoyfQ==",
|
|
128
128
|
"signatures": [
|
|
129
129
|
{
|
|
130
|
-
"keyid": "
|
|
131
|
-
"sig": "
|
|
130
|
+
"keyid": "bb208e67e605dc54aad7c6d6d05cbe6e9531f4d6ef70edd50729f82199fcc5d1",
|
|
131
|
+
"sig": "D1sCDTur/vV9Agzys9RNyRQjC/z4xJU+Ch1rWRksCx8ISP54+EEL9PlDEKZrAigX6hSim1zai42ssgaR+SdGBQ=="
|
|
132
132
|
}
|
|
133
133
|
]
|
|
134
134
|
},
|
package/vectors/canonical.json
CHANGED
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
}
|
|
54
54
|
],
|
|
55
55
|
"keyid": {
|
|
56
|
-
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\
|
|
57
|
-
"keyid": "
|
|
56
|
+
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA9PR1NM7CAYV6ztqdIxlV8EtbwKt/noC1QE1/uu64w8A=\n-----END PUBLIC KEY-----\n",
|
|
57
|
+
"keyid": "4b54e3508693bbc3e6236cd42834826ba45205a4e4d3b14a645e12e0d4cc0901"
|
|
58
58
|
},
|
|
59
59
|
"dsse": {
|
|
60
|
-
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\
|
|
60
|
+
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA9PR1NM7CAYV6ztqdIxlV8EtbwKt/noC1QE1/uu64w8A=\n-----END PUBLIC KEY-----\n",
|
|
61
61
|
"envelope": {
|
|
62
62
|
"payloadType": "application/vnd.example+json",
|
|
63
63
|
"payload": "eyJoZWxsbyI6IndvcmxkIn0=",
|
|
64
64
|
"signatures": [
|
|
65
65
|
{
|
|
66
|
-
"keyid": "
|
|
67
|
-
"sig": "
|
|
66
|
+
"keyid": "4b54e3508693bbc3e6236cd42834826ba45205a4e4d3b14a645e12e0d4cc0901",
|
|
67
|
+
"sig": "/xY/5ueLIFwQYepoDm4sOGtxO9MntKEbYV86DwPlcosuIbcc92J2SdItEeSQdE54v5N/LTm2iqk6jMCl1rLVAw=="
|
|
68
68
|
}
|
|
69
69
|
]
|
|
70
70
|
},
|