@agent-custody/receipts 0.2.0 → 0.3.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/dist/otel.js ADDED
@@ -0,0 +1,87 @@
1
+ const str = (key, v) => (v === null || v === undefined ? [] : [{ key, value: { stringValue: v } }]);
2
+ const int = (key, v) => [{ key, value: { intValue: String(v) } }];
3
+ /** The span for one receipt, as the OTLP JSON a collector accepts on /v1/traces. Exported so a test or another transport can reuse it. */
4
+ export function spanFor(p, bundle, serviceName) {
5
+ const hex = p.receiptId.replace(/-/g, "");
6
+ const traceId = hex.length === 32 ? hex : hex.padEnd(32, "0").slice(0, 32);
7
+ const start = BigInt(Date.parse(p.timestamp)) * 1000000n;
8
+ const end = BigInt(Date.now()) * 1000000n;
9
+ const status = p.execution.status;
10
+ const auth = p.authorization;
11
+ const attributes = [
12
+ ...str("agent_custody.receipt_id", p.receiptId),
13
+ ...str("agent_custody.issuer.kind", p.issuer.kind),
14
+ ...str("agent_custody.issuer.keyid", p.issuer.keyid),
15
+ ...str("agent_custody.tool", p.tool.name),
16
+ ...str("agent_custody.upstream", p.tool.upstream),
17
+ ...str("agent_custody.agent", p.agent.id),
18
+ ...str("agent_custody.agent.provenance", p.agent.provenance),
19
+ ...str("agent_custody.principal", p.principal.id),
20
+ ...str("agent_custody.execution.status", status),
21
+ ...str("agent_custody.policy.decision", p.policy?.decision),
22
+ ...str("agent_custody.policy.digest", p.policy?.policyDigest),
23
+ ...int("agent_custody.log.leaf_index", bundle.inclusion.leafIndex),
24
+ ...int("agent_custody.log.tree_size", bundle.inclusion.treeSize),
25
+ ...(auth ? int("agent_custody.authorization.leaf_index", auth.inclusion.leafIndex) : []),
26
+ ...int("agent_custody.consumed.count", p.consumed?.factIds.length ?? 0),
27
+ ...str("agent_custody.model", p.model.id),
28
+ ...str("agent_custody.session", p.session.id),
29
+ ];
30
+ return {
31
+ resourceSpans: [
32
+ {
33
+ resource: { attributes: str("service.name", serviceName) },
34
+ scopeSpans: [
35
+ {
36
+ scope: { name: "agent-custody", version: p.issuer.version },
37
+ spans: [
38
+ {
39
+ traceId,
40
+ spanId: traceId.slice(0, 16),
41
+ name: `${p.tool.name}`,
42
+ kind: 3, // CLIENT: the agent calling out
43
+ startTimeUnixNano: start.toString(),
44
+ endTimeUnixNano: (end > start ? end : start).toString(),
45
+ attributes,
46
+ status: status === "executed" || status === "denied" || status === "withheld" ? { code: 1 } : { code: 2, message: status === "failed" ? "the upstream reported failure" : p.execution.error ?? status },
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ };
54
+ }
55
+ /** An exporter that posts each receipt's span to an OTLP/HTTP collector. Failures are reported, never thrown. */
56
+ export function otlpExporter(cfg, opts = {}) {
57
+ const f = opts.fetch ?? fetch;
58
+ const env = opts.env ?? process.env;
59
+ const warn = opts.warn ?? ((m) => console.error(m));
60
+ const headers = { "content-type": "application/json" };
61
+ for (const [header, variable] of Object.entries(cfg.headersEnv ?? {})) {
62
+ const v = env[variable];
63
+ if (!v)
64
+ throw new Error(`otel: environment variable ${variable} is not set`);
65
+ headers[header] = v;
66
+ }
67
+ const base = cfg.url.endsWith("/") ? cfg.url : `${cfg.url}/`;
68
+ const url = new URL("v1/traces", base);
69
+ const serviceName = cfg.serviceName ?? "agent-custody";
70
+ return {
71
+ where: cfg.url,
72
+ async exported(p, bundle) {
73
+ try {
74
+ const res = await f(url, { method: "POST", headers, body: JSON.stringify(spanFor(p, bundle, serviceName)), signal: AbortSignal.timeout(5000) });
75
+ if (!res.ok)
76
+ warn(`agent-custody: otel export of receipt ${p.receiptId} refused by ${cfg.url}: ${res.status}`);
77
+ }
78
+ catch (e) {
79
+ warn(`agent-custody: otel export of receipt ${p.receiptId} failed: ${e instanceof Error ? e.message : String(e)}`);
80
+ }
81
+ },
82
+ };
83
+ }
84
+ /** The exporter a config asks for, or undefined. */
85
+ export function openExporter(cfg) {
86
+ return cfg.otel ? otlpExporter(cfg.otel) : undefined;
87
+ }
package/dist/receipt.d.ts CHANGED
@@ -4,6 +4,8 @@ import type { PolicyDecision } from "./policy.ts";
4
4
  import type { UpstreamEvidence } from "./upstream.ts";
5
5
  export declare const RECEIPT_TYPE = "application/vnd.in-toto+json";
6
6
  export declare const RECEIPT_PREDICATE_TYPE = "https://agent-custody.dev/receipt/v0.2";
7
+ /** The statement a gateway commits to the log before forwarding a consequential call; the receipt for that call embeds it. */
8
+ export declare const AUTHORIZATION_PREDICATE_TYPE = "https://agent-custody.dev/authorization/v0.1";
7
9
  export declare const TREEHEAD_TYPE = "application/vnd.agent-custody.treehead+json";
8
10
  /**
9
11
  * Provenance of a receipt field. This is the honest part of the design.
@@ -86,6 +88,13 @@ export interface ReceiptPredicate {
86
88
  policy: (PolicyDecision & {
87
89
  provenance: Provenance;
88
90
  }) | null;
91
+ /**
92
+ * Present when the tool is one the gateway treats as consequential: the authorization statement it committed to the
93
+ * log before forwarding the call, with that leaf's inclusion proof and tree head. A verifier checks that it names
94
+ * this receipt, this tool, and these arguments, and that its leaf precedes the receipt's. Without it, evidence of a
95
+ * side effect exists only after the side effect.
96
+ */
97
+ authorization?: AuthorizationBundle;
89
98
  execution: {
90
99
  status: "executed" | "failed";
91
100
  result: unknown;
@@ -101,8 +110,34 @@ export interface ReceiptPredicate {
101
110
  status: "error";
102
111
  error: string;
103
112
  provenance: Provenance;
113
+ }
114
+ /** the policy allowed the call but the gateway did not forward it, because the log would not commit the authorization first */
115
+ | {
116
+ status: "withheld";
117
+ reason: string;
118
+ provenance: Provenance;
104
119
  };
105
120
  }
121
+ /** What the gateway commits before a consequential call goes out: everything the receipt will say, except the outcome. */
122
+ export type AuthorizationPredicate = Pick<ReceiptPredicate, "receiptId" | "timestamp" | "issuer" | "principal" | "agent" | "delegation" | "tool" | "request" | "facts" | "consumed" | "policy">;
123
+ export interface AuthorizationStatement {
124
+ _type: "https://in-toto.io/Statement/v1";
125
+ subject: {
126
+ name: string;
127
+ digest: {
128
+ sha256: string;
129
+ };
130
+ }[];
131
+ predicateType: typeof AUTHORIZATION_PREDICATE_TYPE;
132
+ predicate: AuthorizationPredicate;
133
+ }
134
+ /** The committed authorization: the same three parts as a receipt bundle. Written to `receipts/<receiptId>.authorization.json` and embedded in the receipt. */
135
+ export interface AuthorizationBundle {
136
+ envelope: Envelope;
137
+ treeHead: Envelope;
138
+ inclusion: InclusionProof;
139
+ }
140
+ export declare function buildAuthorizationStatement(p: AuthorizationPredicate): AuthorizationStatement;
106
141
  export interface ReceiptStatement {
107
142
  _type: "https://in-toto.io/Statement/v1";
108
143
  subject: {
package/dist/receipt.js CHANGED
@@ -1,6 +1,16 @@
1
1
  export const RECEIPT_TYPE = "application/vnd.in-toto+json";
2
2
  export const RECEIPT_PREDICATE_TYPE = "https://agent-custody.dev/receipt/v0.2";
3
+ /** The statement a gateway commits to the log before forwarding a consequential call; the receipt for that call embeds it. */
4
+ export const AUTHORIZATION_PREDICATE_TYPE = "https://agent-custody.dev/authorization/v0.1";
3
5
  export const TREEHEAD_TYPE = "application/vnd.agent-custody.treehead+json";
6
+ export function buildAuthorizationStatement(p) {
7
+ return {
8
+ _type: "https://in-toto.io/Statement/v1",
9
+ subject: [{ name: `tool-call:${p.tool.name}:${p.receiptId}`, digest: { sha256: p.request.argsDigest } }],
10
+ predicateType: AUTHORIZATION_PREDICATE_TYPE,
11
+ predicate: p,
12
+ };
13
+ }
4
14
  export function buildStatement(p) {
5
15
  return {
6
16
  _type: "https://in-toto.io/Statement/v1",
package/dist/rest.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { RestToolConfig, RestUpstreamConfig } from "./config.ts";
3
+ /** The part of an MCP client the gateway uses, so a REST connector can stand where an MCP upstream stands. */
4
+ export interface UpstreamClient {
5
+ listTools(): Promise<{
6
+ tools: Tool[];
7
+ }>;
8
+ callTool(params: {
9
+ name: string;
10
+ arguments?: Record<string, unknown>;
11
+ _meta?: Record<string, unknown>;
12
+ }): Promise<unknown>;
13
+ close(): Promise<void>;
14
+ }
15
+ export interface RestOptions {
16
+ fetch?: typeof fetch;
17
+ /** the environment the header secrets are read from; default process.env */
18
+ env?: Record<string, string | undefined>;
19
+ }
20
+ /** Builds the request for one tool call: path parameters substituted, the rest of the arguments as query or body. */
21
+ export declare function buildRequest(base: string, tool: RestToolConfig, args: Record<string, unknown>): {
22
+ url: URL;
23
+ method: string;
24
+ body: string | null;
25
+ };
26
+ /** A REST API as an upstream. Tools are what the config declares; credentials come from the environment at startup, never from the agent. */
27
+ export declare function restUpstream(name: string, cfg: RestUpstreamConfig, opts?: RestOptions): UpstreamClient;
package/dist/rest.js ADDED
@@ -0,0 +1,52 @@
1
+ const encode = (v) => encodeURIComponent(typeof v === "string" ? v : JSON.stringify(v));
2
+ /** Builds the request for one tool call: path parameters substituted, the rest of the arguments as query or body. */
3
+ export function buildRequest(base, tool, args) {
4
+ const rest = { ...args };
5
+ const path = tool.path.replace(/\{([A-Za-z0-9_]+)\}/g, (_, name) => {
6
+ if (!(name in rest) || rest[name] === undefined)
7
+ throw new Error(`tool ${tool.name}: path needs argument "${name}", which is missing`);
8
+ const v = rest[name];
9
+ delete rest[name];
10
+ return encode(v);
11
+ });
12
+ const url = new URL(path.replace(/^\//, ""), base.endsWith("/") ? base : `${base}/`);
13
+ const method = tool.method;
14
+ const queryNames = tool.query ?? (method === "GET" || method === "DELETE" ? Object.keys(rest) : []);
15
+ for (const name of queryNames) {
16
+ if (rest[name] === undefined)
17
+ continue;
18
+ url.searchParams.set(name, typeof rest[name] === "string" ? rest[name] : JSON.stringify(rest[name]));
19
+ delete rest[name];
20
+ }
21
+ const body = method === "GET" || method === "DELETE" || tool.body === "none" ? null : JSON.stringify(rest);
22
+ return { url, method, body };
23
+ }
24
+ /** A REST API as an upstream. Tools are what the config declares; credentials come from the environment at startup, never from the agent. */
25
+ export function restUpstream(name, cfg, opts = {}) {
26
+ const f = opts.fetch ?? fetch;
27
+ const env = opts.env ?? process.env;
28
+ const headers = { ...(cfg.headers ?? {}) };
29
+ for (const [header, variable] of Object.entries(cfg.headerEnv ?? {})) {
30
+ const v = env[variable];
31
+ if (!v)
32
+ throw new Error(`upstream ${name}: environment variable ${variable} is not set`);
33
+ headers[header] = v;
34
+ }
35
+ const tools = new Map(cfg.tools.map((t) => [t.name, t]));
36
+ return {
37
+ async listTools() {
38
+ return { tools: cfg.tools.map((t) => ({ name: t.name, description: t.description ?? `${t.method} ${t.path}`, inputSchema: t.inputSchema })) };
39
+ },
40
+ async callTool(params) {
41
+ const tool = tools.get(params.name);
42
+ if (!tool)
43
+ throw new Error(`upstream ${name} has no tool "${params.name}"`);
44
+ const { url, method, body } = buildRequest(cfg.baseUrl, tool, params.arguments ?? {});
45
+ const res = await f(url, { method, headers: { accept: "application/json", ...(body !== null ? { "content-type": "application/json" } : {}), ...headers }, ...(body !== null ? { body } : {}), signal: AbortSignal.timeout(cfg.timeoutMs) });
46
+ const text = await res.text();
47
+ // The result is what the API returned, as text. JSON stays JSON so policy facts and consumers can read it.
48
+ return { content: [{ type: "text", text: text.length ? text : JSON.stringify({ status: res.status }) }], ...(res.ok ? {} : { isError: true }) };
49
+ },
50
+ async close() { },
51
+ };
52
+ }
package/dist/sdk/index.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { digestOf, loadPrivateKey } from "../crypto.js";
6
6
  import { createIssuer } from "../issue.js";
7
+ import { openExporter } from "../otel.js";
7
8
  import { openLog } from "../log-sink.js";
8
9
  import { evaluate } from "../policy.js";
9
10
  export const SDK_VERSION = "0.1.0";
@@ -21,7 +22,7 @@ export class PolicyDeniedError extends Error {
21
22
  }
22
23
  export function createSdkIssuer(cfg) {
23
24
  const key = loadPrivateKey(cfg.identity.keyFile);
24
- const issuer = createIssuer(key, cfg.receiptsDir, openLog(cfg, key));
25
+ const issuer = createIssuer(key, cfg.receiptsDir, openLog(cfg, key), { exporter: openExporter(cfg) });
25
26
  const policyText = cfg.policyFile ? readFileSync(cfg.policyFile, "utf8") : null;
26
27
  const decide = (ev) => policyText === null ? null : evaluate(policyText, { agentId: cfg.agentId, tool: ev.tool, context: { args: ev.args, facts: {} } });
27
28
  function record(ev, outcome, policy = null) {
package/dist/verify.js CHANGED
@@ -3,7 +3,7 @@ import { canonicalize, digestOf, dsseVerify } from "./crypto.js";
3
3
  import { delegationValidAt, 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
- import { RECEIPT_PREDICATE_TYPE, RECEIPT_TYPE, TREEHEAD_TYPE } from "./receipt.js";
6
+ import { AUTHORIZATION_PREDICATE_TYPE, RECEIPT_PREDICATE_TYPE, RECEIPT_TYPE, TREEHEAD_TYPE } from "./receipt.js";
7
7
  const short = (s) => s.slice(0, 12);
8
8
  export function verifyBundle(bundle, opts) {
9
9
  const checks = [];
@@ -64,6 +64,29 @@ export function verifyBundle(bundle, opts) {
64
64
  add("no policy errors on an allow", !(p.policy.decision === "allow" && p.policy.errors.length > 0));
65
65
  }
66
66
  const logKeys = opts.logKeys ?? [];
67
+ if (p.authorization) {
68
+ // The gateway says it committed this call to the log before forwarding it. Check that the committed statement is
69
+ // the issuer's, describes this very call, sits in the log, and sits there before the receipt does.
70
+ const a = p.authorization;
71
+ const asig = dsseVerify(a.envelope, opts.issuerKeys);
72
+ const ast = asig.ok ? asig.payload : null;
73
+ const typed = !!ast && a.envelope.payloadType === RECEIPT_TYPE && ast.predicateType === AUTHORIZATION_PREDICATE_TYPE;
74
+ add("authorization signature (issuer key)", asig.ok && typed && asig.keyid === sig.keyid, asig.ok ? (typed ? `keyid ${short(asig.keyid)}` : "not an authorization statement") : asig.error);
75
+ if (ast && typed) {
76
+ const ap = ast.predicate;
77
+ const same = ap.receiptId === p.receiptId && ap.tool.name === p.tool.name && ap.request.argsDigest === p.request.argsDigest && ap.agent.id === p.agent.id && ap.principal.id === p.principal.id && ap.policy?.decision === "allow";
78
+ add("authorization names this call", same, same ? `${ap.tool.name} for receipt ${short(ap.receiptId)}` : "committed for a different receipt, tool, arguments, agent, or decision");
79
+ const ath = dsseVerify(a.treeHead, [...logKeys, ...opts.issuerKeys]);
80
+ add("authorization tree head signature", ath.ok && a.treeHead.payloadType === TREEHEAD_TYPE, ath.ok ? `keyid ${short(ath.keyid)}` : ath.error);
81
+ if (ath.ok) {
82
+ const ahead = ath.payload;
83
+ const included = ahead.treeSize === a.inclusion.treeSize && verifyInclusion(leafHash(canonicalize(a.envelope)), a.inclusion, ahead.rootHash);
84
+ add("authorization log inclusion proof", included, `leaf ${a.inclusion.leafIndex} of ${a.inclusion.treeSize}`);
85
+ const before = a.inclusion.leafIndex < bundle.inclusion.leafIndex && a.inclusion.treeSize <= bundle.inclusion.treeSize;
86
+ add("authorization logged before execution", before, `authorization leaf ${a.inclusion.leafIndex}, receipt leaf ${bundle.inclusion.leafIndex}`);
87
+ }
88
+ }
89
+ }
67
90
  const th = dsseVerify(bundle.treeHead, [...logKeys, ...opts.issuerKeys]);
68
91
  const byLog = th.ok && logKeys.some((k) => k.keyid === th.keyid);
69
92
  add("tree head signature", th.ok && bundle.treeHead.payloadType === TREEHEAD_TYPE, th.ok ? `${byLog ? "log key" : "issuer key"} ${short(th.keyid)}` : th.error);
@@ -145,6 +168,12 @@ export function formatReport(r) {
145
168
  row("policy", "-", "(none evaluated)");
146
169
  if (p.consumed)
147
170
  row("consumed", p.consumed.provenance, p.consumed.factIds.length === 0 ? "(no facts shown before this call)" : p.consumed.factIds);
171
+ if (p.authorization) {
172
+ const committed = r.checks.filter((c) => c.name.startsWith("authorization ")).every((c) => c.ok);
173
+ row("authorization", "observed", committed ? `committed to the log as leaf ${p.authorization.inclusion.leafIndex}, before the call was forwarded` : "carried, but its checks FAILED");
174
+ }
175
+ else if (p.execution.status === "withheld")
176
+ row("authorization", "observed", "the log would not commit it; the call was not forwarded");
148
177
  const upstreamCheck = r.checks.find((c) => c.name === "upstream signature (upstream key)" || c.name === "upstream signature (provider secret)");
149
178
  const hasUpstream = (p.execution.status === "executed" || p.execution.status === "failed") && !!p.execution.upstream;
150
179
  const byProvider = hasUpstream && isProviderAttestation(p.execution.upstream);
package/docs/sdk.md CHANGED
@@ -30,6 +30,8 @@ Use the SDK for reach. Use the gateway for anything that moves money, touches pr
30
30
 
31
31
  `policyFile` and `principalId` are optional. Without a policy the SDK records and never denies. Paths resolve relative to the config file. Instead of `logFile`, `"log": { "url": "https://log.example.com/", "tokenEnv": "AGENT_CUSTODY_LOG_TOKEN" }` sends every leaf to a log run by someone else, whose key then signs the tree heads; see [usage.md](usage.md) for what that changes and [verification.md](verification.md) for what it proves. Generate the key with `node src/cli.ts keygen --dir keys --name app`.
32
32
 
33
+ `"otel": { "url": "http://localhost:4318" }` additionally exports every receipt as one span to that OTLP/HTTP collector, after the receipt is written, with the receipt id as the trace id; see the [usage guide](usage.md#setup-step-by-step) for the fields. Export never blocks or fails a receipt.
34
+
33
35
  Policies see `context.args` and an empty `context.facts`. A policy that reads `context.facts` or `context.grant` errors, which is a deny. That is intended: an SDK policy cannot pretend it checked something outside the agent's process.
34
36
 
35
37
  ## Claude Code
package/docs/tutorials.md CHANGED
@@ -25,6 +25,9 @@ Suggested reading order is the numbering. Output lands in `examples-out/`, which
25
25
  | 13 | a log run by someone else | [13-remote-log.ts](../examples/13-remote-log.ts) | the reference log server on a free port, an SDK config that logs to it, a tree head signed by the log's key, verification failing without that key and passing with it, the root endpoint, a refused token | `src/log-sink.ts` |
26
26
  | 14 | proving history was not rewritten | [14-audit-history.ts](../examples/14-audit-history.ts) | three receipts and a kept tree head, a consistency proof that passes, the operator rewriting one leaf and appending a fourth call, the audit failing while the fourth receipt still verifies alone | `src/log.ts`, `src/verify.ts` |
27
27
  | 15 | agents in other languages | [15-sidecar.ts](../examples/15-sidecar.ts) | the sidecar on a free port, a client written as a Python or Go program would write it: decide, run, record; a denial recorded without running the tool; both receipts verified | `src/sidecar.ts` |
28
+ | 16 | consequential tools, committed first | [16-precommit.ts](../examples/16-precommit.ts) | a refund named in `precommit`: the authorization leaf before the receipt leaf, the five authorization checks in the report, and the same call withheld when the log refuses | `src/gateway.ts`, `src/issue.ts`, `src/verify.ts` |
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
+ | 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` |
28
31
 
29
32
  ## How policies are defined, in one paragraph
30
33
 
package/docs/usage.md CHANGED
@@ -72,6 +72,7 @@ when {
72
72
  "forTools": ["stripe.refund"]
73
73
  }
74
74
  ],
75
+ "precommit": ["stripe.refund"],
75
76
  "receiptsDir": "receipts",
76
77
  "logFile": "log.jsonl"
77
78
  }
@@ -79,16 +80,39 @@ when {
79
80
 
80
81
  `upstream` is spawned by the gateway exactly as an MCP host would spawn it. `env` is passed through, which is where upstream credentials go. The agent never sees them. Several upstreams sit behind one gateway and one grant with `"upstreams": [{ "name": "memory", "command": ..., "args": [...] }, { "name": "payments", "url": ... }]` in place of `upstream`. Each tool name must be offered by exactly one of them, checked at startup; the receipt's `tool.upstream` says which served the call, and consumed facts flow across them, so a refund made after a memory read carries the facts the agent had been shown. An upstream that is already running is reached instead with `"upstream": { "url": "https://memory.internal/mcp", "tokenEnv": "MEMORY_TOKEN" }`, over Streamable HTTP with a bearer token from the environment; the shared memory server in `@agent-custody/state` is the usual case.
81
82
 
83
+ An upstream need not be an MCP server. A plain HTTP API is described as tools:
84
+
85
+ ```json
86
+ "upstream": {
87
+ "rest": {
88
+ "baseUrl": "https://api.stripe.com",
89
+ "headerEnv": { "authorization": "STRIPE_BEARER" },
90
+ "tools": [
91
+ { "name": "customer.lookup", "method": "GET", "path": "/v1/customers/{customer_id}",
92
+ "inputSchema": { "type": "object", "properties": { "customer_id": { "type": "string" } }, "required": ["customer_id"] } },
93
+ { "name": "stripe.refund", "method": "POST", "path": "/v1/refunds", "description": "Refund a customer, amount in minor units",
94
+ "inputSchema": { "type": "object", "properties": { "customer_id": { "type": "string" }, "amount": { "type": "integer" } }, "required": ["customer_id", "amount"] } }
95
+ ]
96
+ }
97
+ }
98
+ ```
99
+
100
+ `{name}` segments in `path` are filled from the call's arguments; the remaining arguments go to the query string on GET and DELETE and to a JSON body otherwise, or `query` names the ones that go to the query and `body: "none"` sends no body. `headers` are sent as written; `headerEnv` maps a header to an environment variable read once at startup, so the credential is never in the file and never reaches the agent, and a missing variable fails at startup. The response body is the tool result, JSON kept as JSON, and a non-2xx status is a failed execution with the API's answer in the receipt. Everything else is unchanged: the tools appear in the grant's scopes, `facts` may name a REST tool for a lookup, `precommit` applies, and each call has a receipt. `rest` can be one of several `upstreams` beside MCP servers. This is how an agent's direct HTTP calls come under custody: they become tool calls through the gateway. Tutorial 17 runs one against a stand-in API.
101
+
82
102
  `logFile` is the local Merkle log, with tree heads signed by the gateway's own key. To log to a server the operator does not control, replace it with `log`:
83
103
 
84
104
  ```json
85
105
  "log": { "url": "https://log.example.com/", "tokenEnv": "AGENT_CUSTODY_LOG_TOKEN" }
86
106
  ```
87
107
 
88
- Exactly one of the two. The bearer token comes from the named environment variable, never from the file, and a missing variable fails at startup. With a remote log the tree head in each receipt is signed by the log's key, and a verifier must be given that key with `--log-key`. If the log refuses a leaf, the receipt is not issued and the call returns an error to the agent; for an executed call the upstream action has already happened by then, which is the honest outcome, since a receipt that was never logged must not be handed out. The reference log server is `node src/cli.ts log --file log.jsonl --key keys/log.key --port 8787 --token-env AGENT_CUSTODY_LOG_TOKEN`. It serves `POST /append` (token required when one is configured), `GET /root?size=N`, `GET /consistency?old=M&new=N`, and `GET /head`; [verification.md](verification.md) says what each proves.
108
+ Exactly one of the two. The bearer token comes from the named environment variable, never from the file, and a missing variable fails at startup. With a remote log the tree head in each receipt is signed by the log's key, and a verifier must be given that key with `--log-key`. If the log refuses a leaf, the receipt is not issued and the call returns an error to the agent. For an ordinary call the upstream action has already happened by then, and the error says so; a receipt that was never logged must not be handed out. For a tool named in `precommit` the order is reversed, below, and the action never happens. The reference log server is `node src/cli.ts log --file log.jsonl --key keys/log.key --port 8787 --token-env AGENT_CUSTODY_LOG_TOKEN`. It serves `POST /append` (token required when one is configured), `GET /root?size=N`, `GET /consistency?old=M&new=N`, and `GET /head`; [verification.md](verification.md) says what each proves.
109
+
110
+ `otel`, optional in both the gateway and SDK configs, sends every receipt to the collector you already run as one span over OTLP/HTTP, after the receipt is issued: `"otel": { "url": "http://localhost:4318", "headersEnv": { "x-api-key": "OTEL_KEY" }, "serviceName": "support-agents" }`. The span's trace id is the receipt id, its attributes carry the tool, agent, principal, execution status, policy decision, and log position, and its status is an error only when the upstream failed or errored, since a denial is the policy working. Export is best effort: a collector that is down or refuses costs a line on stderr, never a receipt. Tutorial 18 shows it against a stand-in collector.
89
111
 
90
112
  `facts` tells the gateway which upstream tool to call before evaluating policy for a given tool. `$args.<key>` copies a value from the intercepted call. The result appears in Cedar as `context.facts.<name>` and in the receipt with its own digest, labelled `observed`. If a fact lookup fails, the call is denied and the receipt says why. A lookup with `"optional": true` is skipped when a `$args.<key>` it needs is absent from the call, and the fact is then simply not present, which a policy tests with `context.facts has <name>`; this is how a policy sees the fact a `memory.write` is about to supersede without denying every write that supersedes nothing.
91
113
 
114
+ `precommit` names the consequential tools, or `["*"]` for all of them. For every other tool the gateway forwards the call and then records it, so if the log is unreachable at that moment the side effect exists before its evidence does. For a tool in `precommit` the gateway first signs an authorization statement, everything the receipt will say except the outcome, and appends it to the log. Only if the log took it does the call go upstream. The receipt then embeds that authorization with its own inclusion proof, and a verifier checks that it names this call and sits in the log before the receipt does. If the log will not take it, the call is not forwarded, the agent is told `Not executed`, and the receipt records `execution.status: "withheld"` with the policy's `allow` beside it. The authorization is also written on its own as `receipts/<receiptId>.authorization.json`, which is the evidence that survives if the gateway dies between forwarding and the receipt. Use it for money, for anything irreversible, and for anything a counterparty could later dispute; the cost is one extra log append per call.
115
+
92
116
  **5. Run the gateway.** It speaks MCP on stdin/stdout and logs to stderr only.
93
117
 
94
118
  ```bash
@@ -203,5 +227,6 @@ The upstream can answer in kind. A result whose `_meta` carries `agent-custody/f
203
227
  - **Money is integer minor units.** Cedar has no floating point. A float in `args` that a policy touches is an evaluation error, which is a deny.
204
228
  - **The gateway key is the trust root for receipts.** Keep it out of the agent's reach. The upstream credentials in `upstream.env` are likewise never exposed to the agent.
205
229
  - **Rotate keys by adding, not replacing.** The verifier accepts a list of gateway keys and principal keys and matches by keyid, so old receipts stay verifiable.
230
+ - **Evidence before the side effect, for the calls that matter.** Without `precommit`, a call is forwarded and then logged, and a log outage at that moment leaves an executed action with no receipt (the agent gets an error saying so). With `precommit`, the authorization is logged first and the call is withheld if that fails. The receipt of a withheld call shows `allow` next to `withheld`, so an auditor can tell a log outage from a denial.
206
231
  - **The log is append-only by convention, not enforcement.** Copy it somewhere the operator cannot rewrite, on a schedule. The receipts' tree heads let an auditor check that the copy matches.
207
232
  - **stdout is the MCP channel.** Anything the gateway prints goes to stderr. Do not add `console.log` to gateway code paths.
@@ -77,12 +77,17 @@ execution observed executed
77
77
  | request args digest | the args in the predicate hash to the digest in the subject | edited arguments |
78
78
  | 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 |
79
79
  | no policy errors on an allow | an allow was not produced while Cedar reported errors | broken fail-closed behaviour |
80
+ | authorization signature (issuer key) | pre-commit receipts only: the embedded authorization statement was signed by the same issuer key as the receipt | an authorization from another gateway, or none the issuer signed |
81
+ | authorization names this call | the committed authorization names this receipt id, tool, argument digest, agent, and principal, and recorded an allow | an authorization spliced in from a different call |
82
+ | authorization tree head signature | the authorization's own tree head is signed by a trusted log or issuer key | a fabricated log position for the authorization |
83
+ | authorization log inclusion proof | the authorization statement is a leaf of that tree | an authorization that was never logged |
84
+ | authorization logged before execution | the authorization's leaf precedes the receipt's leaf, at a tree size no larger than the receipt's | evidence written after the side effect, dressed up as before |
80
85
  | tree head signature | the tree head was signed by a trusted issuer key | forged log position |
81
86
  | tree head matches inclusion proof size | the proof and the tree head describe the same tree | mismatched bundle parts |
82
87
  | log inclusion proof | this exact envelope is a leaf of the tree with that root | receipt never logged, or logged then changed |
83
88
  | 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 |
84
89
 
85
- 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.
90
+ 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.
86
91
 
87
92
  ## What a verified receipt lets you conclude
88
93
 
@@ -95,6 +100,7 @@ For an SDK receipt the statement is shorter: a process holding the application k
95
100
  A gateway receipt does **not** support:
96
101
 
97
102
  - that Stripe really executed the refund. The upstream result is `observed`, not signed by Stripe. That is the first roadmap item.
103
+ - that the side effect exists because the receipt does, unless the receipt carries an authorization. Without one, the call was forwarded first and logged second. With one, the five `authorization` checks establish that the gateway had committed the call to the log before it went out, and a `withheld` execution establishes that it never went out.
98
104
  - that the arguments were correct. They are `claimed`: they are what the agent asked for, which is what a receipt should record.
99
105
  - that the model named in `model` produced the call. No hosted provider signs model identity.
100
106
  - 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/receipts",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -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": "eyJyb290SGFzaCI6ImFmYjZhMmJjNThiZmQwNDk4YWNjZTZiYTY1MTAxM2IwY2JiYjlmNTI1MWM1NTI0Nzg3NGY5ZWYzOGM3YjAxMjMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ0OVoiLCJ0cmVlU2l6ZSI6MX0=",
10
+ "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
11
11
  "signatures": [
12
12
  {
13
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
14
- "sig": "ZcTaC2tLzE32o+sIj0qFr9lRWaidilkLf6VkVTt6N3McGnp/lISpE0f+naKlRvvQR70kreVm43mwHeMnLBUPCw=="
13
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
14
+ "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
15
15
  }
16
16
  ]
17
17
  },
18
18
  "newer": {
19
19
  "payloadType": "application/vnd.agent-custody.treehead+json",
20
- "payload": "eyJyb290SGFzaCI6IjJlZTc2MmUzYzdlYzdhODE4ODc4MjUwNDhiZGMwZTEyMWFjNDQzM2VjM2FlNjFjYjU5MjM3MWNmYWMxNDVjZmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ1M1oiLCJ0cmVlU2l6ZSI6Mn0=",
20
+ "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
21
21
  "signatures": [
22
22
  {
23
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
24
- "sig": "++9Ra+7i8vejqY418WnWH4CXA9Rxgb/gDHWrEWgZLYpSPjOdbEXB3XlEArIKaP3+I19eTjK4EQQ07mI2WM3ZCQ=="
23
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
24
+ "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
25
25
  }
26
26
  ]
27
27
  },
28
28
  "proof": [
29
- "ae2d0fcc4034cc32db9e07e9f039358baaef3ae6e22533b9c273ee9042a51d5b"
29
+ "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
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": "eyJyb290SGFzaCI6IjJlZTc2MmUzYzdlYzdhODE4ODc4MjUwNDhiZGMwZTEyMWFjNDQzM2VjM2FlNjFjYjU5MjM3MWNmYWMxNDVjZmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ1M1oiLCJ0cmVlU2l6ZSI6Mn0=",
44
+ "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
45
45
  "signatures": [
46
46
  {
47
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
48
- "sig": "++9Ra+7i8vejqY418WnWH4CXA9Rxgb/gDHWrEWgZLYpSPjOdbEXB3XlEArIKaP3+I19eTjK4EQQ07mI2WM3ZCQ=="
47
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
48
+ "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
49
49
  }
50
50
  ]
51
51
  },
52
52
  "newer": {
53
53
  "payloadType": "application/vnd.agent-custody.treehead+json",
54
- "payload": "eyJyb290SGFzaCI6ImFmYjZhMmJjNThiZmQwNDk4YWNjZTZiYTY1MTAxM2IwY2JiYjlmNTI1MWM1NTI0Nzg3NGY5ZWYzOGM3YjAxMjMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ0OVoiLCJ0cmVlU2l6ZSI6MX0=",
54
+ "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
55
55
  "signatures": [
56
56
  {
57
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
58
- "sig": "ZcTaC2tLzE32o+sIj0qFr9lRWaidilkLf6VkVTt6N3McGnp/lISpE0f+naKlRvvQR70kreVm43mwHeMnLBUPCw=="
57
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
58
+ "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
59
59
  }
60
60
  ]
61
61
  },
62
62
  "proof": [
63
- "ae2d0fcc4034cc32db9e07e9f039358baaef3ae6e22533b9c273ee9042a51d5b"
63
+ "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
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": "eyJyb290SGFzaCI6ImFmYjZhMmJjNThiZmQwNDk4YWNjZTZiYTY1MTAxM2IwY2JiYjlmNTI1MWM1NTI0Nzg3NGY5ZWYzOGM3YjAxMjMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ0OVoiLCJ0cmVlU2l6ZSI6MX0=",
80
+ "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
81
81
  "signatures": [
82
82
  {
83
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
84
- "sig": "ZcTaC2tLzE32o+sIj0qFr9lRWaidilkLf6VkVTt6N3McGnp/lISpE0f+naKlRvvQR70kreVm43mwHeMnLBUPCw=="
83
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
84
+ "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
85
85
  }
86
86
  ]
87
87
  },
88
88
  "newer": {
89
89
  "payloadType": "application/vnd.agent-custody.treehead+json",
90
- "payload": "eyJyb290SGFzaCI6IjJlZTc2MmUzYzdlYzdhODE4ODc4MjUwNDhiZGMwZTEyMWFjNDQzM2VjM2FlNjFjYjU5MjM3MWNmYWMxNDVjZmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ1M1oiLCJ0cmVlU2l6ZSI6Mn0=",
90
+ "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
91
91
  "signatures": [
92
92
  {
93
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
94
- "sig": "++9Ra+7i8vejqY418WnWH4CXA9Rxgb/gDHWrEWgZLYpSPjOdbEXB3XlEArIKaP3+I19eTjK4EQQ07mI2WM3ZCQ=="
93
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
94
+ "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
95
95
  }
96
96
  ]
97
97
  },
98
98
  "proof": [
99
- "ae2d0fcc4034cc32db9e07e9f039358baaef3ae6e22533b9c273ee9042a51d5b"
99
+ "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
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": "eyJyb290SGFzaCI6ImFmYjZhMmJjNThiZmQwNDk4YWNjZTZiYTY1MTAxM2IwY2JiYjlmNTI1MWM1NTI0Nzg3NGY5ZWYzOGM3YjAxMjMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ0OVoiLCJ0cmVlU2l6ZSI6MX0=",
117
+ "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
118
118
  "signatures": [
119
119
  {
120
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
121
- "sig": "ZcTaC2tLzE32o+sIj0qFr9lRWaidilkLf6VkVTt6N3McGnp/lISpE0f+naKlRvvQR70kreVm43mwHeMnLBUPCw=="
120
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
121
+ "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
122
122
  }
123
123
  ]
124
124
  },
125
125
  "newer": {
126
126
  "payloadType": "application/vnd.agent-custody.treehead+json",
127
- "payload": "eyJyb290SGFzaCI6IjJlZTc2MmUzYzdlYzdhODE4ODc4MjUwNDhiZGMwZTEyMWFjNDQzM2VjM2FlNjFjYjU5MjM3MWNmYWMxNDVjZmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDEzOjQ4OjQyLjQ1M1oiLCJ0cmVlU2l6ZSI6Mn0=",
127
+ "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
128
128
  "signatures": [
129
129
  {
130
- "keyid": "402c5fa71d8055eadffe3e33d7847e543551d24c002a9adf86286f03eb5e4538",
131
- "sig": "++9Ra+7i8vejqY418WnWH4CXA9Rxgb/gDHWrEWgZLYpSPjOdbEXB3XlEArIKaP3+I19eTjK4EQQ07mI2WM3ZCQ=="
130
+ "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
131
+ "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
132
132
  }
133
133
  ]
134
134
  },
@@ -53,18 +53,18 @@
53
53
  }
54
54
  ],
55
55
  "keyid": {
56
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAPkUEkHrnkfYttWXvl/rsSvOqIW8k0HvjQknUWhsI0tI=\n-----END PUBLIC KEY-----\n",
57
- "keyid": "b8f05ee956d16bc7e981e1cc6e62739623e3a169d8e14aa43d2c2b32ce956b44"
56
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEABAiIObrUZgS8uRY1zr5NCUnkr2HqN3K/b+P75pkPX1I=\n-----END PUBLIC KEY-----\n",
57
+ "keyid": "f660c2bc47886be244642e60bbe1235946e88b8ca864c1c55ef7a8c7a424e184"
58
58
  },
59
59
  "dsse": {
60
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAPkUEkHrnkfYttWXvl/rsSvOqIW8k0HvjQknUWhsI0tI=\n-----END PUBLIC KEY-----\n",
60
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEABAiIObrUZgS8uRY1zr5NCUnkr2HqN3K/b+P75pkPX1I=\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": "b8f05ee956d16bc7e981e1cc6e62739623e3a169d8e14aa43d2c2b32ce956b44",
67
- "sig": "sSViplEmHbNqomtr8I67F+jIH8JDbmFhGkioDN8nXxpb1yg21/FLP2wqBVHrTMveuCtF4O+EoxO7dlBITeoIBw=="
66
+ "keyid": "f660c2bc47886be244642e60bbe1235946e88b8ca864c1c55ef7a8c7a424e184",
67
+ "sig": "DY3PDf89eYVsOJeQJYxT4W2c4NsNeWup+P+Oi/yvp9Fr34uSct3SfueWkmdaAtrmjrenoAdNt8BWV0N+6f0JDA=="
68
68
  }
69
69
  ]
70
70
  },