@agent-custody/receipts 0.2.0 → 0.4.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 +24 -12
- package/dist/cli.js +26 -6
- package/dist/config.d.ts +134 -0
- package/dist/config.js +34 -2
- package/dist/gateway.d.ts +9 -1
- package/dist/gateway.js +45 -16
- package/dist/index.d.ts +8 -0
- package/dist/index.js +3 -0
- package/dist/issue.d.ts +9 -2
- package/dist/issue.js +13 -2
- package/dist/log-sink.d.ts +15 -0
- package/dist/log-sink.js +31 -18
- package/dist/log.d.ts +8 -0
- package/dist/log.js +17 -3
- package/dist/otel.d.ts +28 -0
- package/dist/otel.js +87 -0
- package/dist/receipt.d.ts +37 -0
- package/dist/receipt.js +10 -0
- package/dist/rest.d.ts +27 -0
- package/dist/rest.js +52 -0
- package/dist/sdk/index.js +2 -1
- package/dist/verify.d.ts +3 -1
- package/dist/verify.js +37 -2
- package/docs/sdk.md +2 -0
- package/docs/tutorials.md +3 -0
- package/docs/usage.md +26 -1
- package/docs/verification.md +9 -2
- package/package.json +1 -1
- package/vectors/audit.json +27 -27
- package/vectors/canonical.json +5 -5
- package/vectors/receipts.json +440 -162
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# agent-custody
|
|
2
2
|
|
|
3
|
-
Chain of custody for AI agents: signed
|
|
3
|
+
Chain of custody for AI agents: a signed receipt for every tool call, checkable with public keys alone. What each check proves, and against whom, is [a table below](#what-a-receipt-proves-and-what-it-does-not).
|
|
4
4
|
|
|
5
5
|
Two producers, one receipt format, one verifier.
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Two producers, one receipt format, one verifier.
|
|
|
9
9
|
|
|
10
10
|
Anyone holding the public keys can verify a receipt offline. The agent is not trusted. The layer around it is, and the receipt says exactly how far that trust extends, starting with who issued it.
|
|
11
11
|
|
|
12
|
-
- [Tutorials](docs/tutorials.md):
|
|
12
|
+
- [Tutorials](docs/tutorials.md): eighteen runnable examples, one per aspect of the code, all executed by the test suite
|
|
13
13
|
- [Usage guide](docs/usage.md): gateway setup, wiring into Claude Desktop, Claude Code, or your own agent loop
|
|
14
14
|
- [The interceptor SDK](docs/sdk.md): Claude Code hooks, the Claude Agent SDK, adapters for the OpenAI Agents SDK, Vercel AI SDK and LangChain, and wrapping tool functions in anything else
|
|
15
15
|
- [Writing policies](docs/policies.md): how a tool call becomes a Cedar request, with tested examples
|
|
@@ -75,6 +75,8 @@ flowchart LR
|
|
|
75
75
|
P -. "public key" .-> V
|
|
76
76
|
G -. "public key" .-> V
|
|
77
77
|
A -. "traces (unchanged)" .-> O
|
|
78
|
+
G -. "one span per receipt, OTLP" .-> O
|
|
79
|
+
S -. "one span per receipt, OTLP" .-> O
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
Three parties hold keys. The **principal** signs a grant saying which agent may use which tools until when. The **issuer**, gateway or SDK, signs every receipt and every tree head. The **verifier** holds only public keys and needs no access to the issuer, the agent, or the upstream system.
|
|
@@ -106,6 +108,7 @@ Every receipt names its issuer, and the verifier prints what that issuer kind is
|
|
|
106
108
|
| Python: LangChain, OpenAI Agents SDK, Claude Agent SDK | sidecar + [Python package](../python/README.md) | `wrap_tools`, `claude_hook` PreToolUse deny, `client.wrap` | `ReceiptCallbackHandler` | the real Python packages, receipts checked by this verifier |
|
|
107
109
|
| Go, Java, Rust, any language with HTTP | sidecar | decide then record | record | [examples/languages](examples/languages), each run against a live sidecar |
|
|
108
110
|
| any MCP host in any language: Claude Agent SDK Python, OpenAI Agents Python | gateway | yes | | the gateway is an MCP server; [usage.md](docs/usage.md#python-hosts) |
|
|
111
|
+
| any REST API, as tools the agent reaches through the gateway | gateway, `rest` upstream | yes | | a stand-in HTTP API; [usage.md](docs/usage.md#setup-step-by-step) |
|
|
109
112
|
|
|
110
113
|
The framework packages are optional peer dependencies. Each adapter imports only from its own package.
|
|
111
114
|
|
|
@@ -170,7 +173,7 @@ Every field carries a provenance label. This is the design decision that matters
|
|
|
170
173
|
bun install # from the repository root, once for the workspace
|
|
171
174
|
cd packages/receipts
|
|
172
175
|
node scripts/demo.ts # gateway: keys, grant, policy, four tool calls, verification, a tampering attempt; then the SDK wrapping the same tool
|
|
173
|
-
node examples/01-keys-and-signing.ts # first of
|
|
176
|
+
node examples/01-keys-and-signing.ts # first of eighteen step-by-step examples, see docs/tutorials.md
|
|
174
177
|
bun run test # this package; `bun run test` at the root runs every package
|
|
175
178
|
```
|
|
176
179
|
|
|
@@ -208,7 +211,8 @@ const refund = issuer.wrap("stripe.refund", async (args: { amount: number }) =>
|
|
|
208
211
|
| the principal really delegated this scope to this agent | anyone with the principal's key | operator, agent | grant signed by principal key, checked for scope and validity window | done |
|
|
209
212
|
| the policy decision was made against this exact policy | anyone | operator swapping policies | sha256 of policy text in the receipt | done |
|
|
210
213
|
| facts the policy relied on were not asserted by the agent | anyone reading the receipt | agent | facts fetched by the gateway, recorded with their own digests, labelled `observed` | done |
|
|
211
|
-
| the
|
|
214
|
+
| the side effect was committed to the log before it happened | anyone with a log copy | operator, log outage | for tools in `precommit`, a signed authorization logged first, embedded in the receipt with its leaf position, checked to precede the receipt | done |
|
|
215
|
+
| the upstream system actually executed the action | anyone with the upstream's key or the provider's secret | operator | the upstream signs its result for the receipt, or the provider's own delivery signature (Stripe, GitHub) is embedded and checked | done, for upstreams that sign |
|
|
212
216
|
| the operator itself cannot mint a false receipt | regulator, counterparty | operator | needs a TEE-hosted signer or a federated log | **not done** |
|
|
213
217
|
| which model produced the call | anyone | operator | no hosted provider signs model identity | **not possible today**, labelled `claimed` |
|
|
214
218
|
| an SDK receipt reflects what the tool really did | anyone | agent's own process | none; the SDK shares a process with the agent | **by design not claimed**; issuer kind `sdk` says so |
|
|
@@ -224,13 +228,15 @@ src/log.ts Merkle log: append, root, inclusion and consistency proofs, v
|
|
|
224
228
|
src/log-sink.ts where leaves go: the local file, or a remote log over HTTP; plus the reference log server
|
|
225
229
|
src/policy.ts Cedar evaluation wrapper, fail-closed
|
|
226
230
|
src/delegation.ts signed delegation grants
|
|
227
|
-
src/receipt.ts receipt statement types and provenance labels
|
|
228
|
-
src/issue.ts sign, log, and write a receipt; shared by both producers
|
|
231
|
+
src/receipt.ts receipt and authorization statement types and provenance labels
|
|
232
|
+
src/issue.ts sign, log, and write a receipt, or commit an authorization first; shared by both producers
|
|
229
233
|
src/gateway.ts the MCP proxy: scope check, facts, policy, forward, receipt
|
|
230
234
|
src/sdk/index.ts the interceptor: policy decision, record, wrap(tool fn)
|
|
231
235
|
src/sdk/claude.ts Claude Code command hook and Claude Agent SDK in-process hooks
|
|
232
236
|
src/sdk/openai-agents.ts, vercel-ai.ts, langchain.ts framework adapters, tested against the real packages
|
|
233
237
|
src/sidecar.ts the SDK issuer behind a local HTTP API, for agents in other languages
|
|
238
|
+
src/otel.ts OpenTelemetry export: one OTLP/HTTP span per receipt, after the receipt, no SDK dependency
|
|
239
|
+
src/rest.ts the REST connector: an HTTP API described as tools, standing where an MCP upstream stands
|
|
234
240
|
src/upstream.ts attested execution: an upstream signs its result for the receipt; the verifier checks it with the upstream key
|
|
235
241
|
vectors/ conformance vectors: receipts, keys, logs, proofs, and expected verdicts; `bun run vectors` regenerates them
|
|
236
242
|
src/verify.ts offline verification, the human-readable report, and the audit that a later log extends an earlier one
|
|
@@ -239,7 +245,7 @@ src/retention.ts pruning the log: leaves become their hashes, bundles are remo
|
|
|
239
245
|
src/index.ts the package's public surface; adapters are exported on ./sdk/<framework> subpaths
|
|
240
246
|
tsconfig.build.json emits dist/ (JavaScript plus declarations) for consumers; the repo itself runs the .ts directly
|
|
241
247
|
scripts/ fake Stripe upstream (signs its results with --key), a second fake upstream, fixture builders for gateway and SDK, demo
|
|
242
|
-
examples/
|
|
248
|
+
examples/ eighteen runnable tutorials, plus examples/languages/: Python, Go, Java, and Rust clients of the sidecar, run by the test suite, one per aspect; each is run by the test suite
|
|
243
249
|
test/ unit tests per module, end-to-end gateway test, SDK and hook tests,
|
|
244
250
|
adapter tests against the real packages, and a test that runs every policy in docs/policies.md
|
|
245
251
|
docs/ tutorials, usage (gateway), sdk, policies, verification
|
|
@@ -271,12 +277,18 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
|
|
|
271
277
|
- Remote log: the issuer can append to a log run by someone else over HTTP, whose key then signs the tree heads, so a verifier learns the receipt was in a log the operator could not rewrite. Includes the reference log server, bearer-token auth, and a root endpoint for auditors.
|
|
272
278
|
- Framework adapters, each tested against the real package with a scripted model and no network: OpenAI Agents SDK (`wrapTools` enforces, `observeRunner` records from lifecycle events), Vercel AI SDK (`wrapTools` over a real `generateText` loop), LangChain (`ReceiptCallbackHandler` records, `issuer.wrap` enforces).
|
|
273
279
|
|
|
280
|
+
- A log for someone else: `hashOnly` sends leaf hashes so the log never holds a receipt; the reference server runs several tenant logs at `/t/<tenant>/` with their own tokens and ids; tree heads name their log and the verifier checks it with `--log-id`. Phase 1 of the hosted log, [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
|
|
281
|
+
- OpenTelemetry export: with `otel` in either config, every receipt is also one span at the collector the team already runs, trace id equal to the receipt id, attributes for tool, agent, principal, status, decision, and log position; after the receipt, best effort, never on the evidence path.
|
|
282
|
+
- The REST connector: a plain HTTP API described as tools in the gateway config, credentials from the environment, so an agent's direct API calls become receipted, policy-checked tool calls through the gateway.
|
|
283
|
+
- Pre-commit authorization for consequential tools: named in `precommit`, a call is signed and logged before it is forwarded, withheld if the log will not take it, and its receipt carries the committed authorization with proof that it precedes the execution.
|
|
284
|
+
|
|
274
285
|
**Next, in the order it pays off**
|
|
275
286
|
|
|
276
|
-
1.
|
|
277
|
-
2.
|
|
278
|
-
3.
|
|
279
|
-
4.
|
|
280
|
-
5.
|
|
287
|
+
1. The hosted log, [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6), phases 2 and 3: the Postgres store with one writer per tenant, tokens and rate limits, the signer as its own process, published checkpoints, and well-known keys fetched with `--log-url`. Phase 1 is done; it comes before everything below.
|
|
288
|
+
2. Post-quantum signatures: ML-DSA beside Ed25519 in the same DSSE envelope, hybrid by default when a PQ key is present, in every signed artefact and in the browser verifier. [Issue #11](https://github.com/ch4r10t33r/agent-custody/issues/11).
|
|
289
|
+
3. An HTTP transport for the gateway, with the grant presented per connection, for a shared deployment rather than one process per agent session.
|
|
290
|
+
4. Delegation chains for sub-agents.
|
|
291
|
+
5. Receiver-attested receipts for agent-to-agent calls.
|
|
292
|
+
6. A TEE-hosted signer, then SD-JWT redaction, then ZK proofs of policy compliance. Not before.
|
|
281
293
|
|
|
282
294
|
A Python SDK follows the same shape once the TypeScript adapters have settled.
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { loadConfig, loadSdkConfig } from "./config.js";
|
|
5
6
|
import { generateKeyPair, loadPrivateKey, loadPublicKey, writeKeyPair } from "./crypto.js";
|
|
6
7
|
import { createDelegation } from "./delegation.js";
|
|
@@ -29,10 +30,24 @@ const USAGE = `agent-custody <command>
|
|
|
29
30
|
prune --log <log.jsonl> --before <ISO instant> [--receipts <dir>]
|
|
30
31
|
retention on the receipt log: replaces older leaves with their hashes, so proofs still verify and the content is gone
|
|
31
32
|
log --file <log.jsonl> --key <log.key> [--port 8787] [--host 127.0.0.1] [--token-env <NAME>] reference log server
|
|
32
|
-
verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--upstream-key <pub>] [--stripe-secret-env NAME] [--github-secret-env NAME] [--log <log.jsonl>] [--json]
|
|
33
|
-
audit --older <bundle.json> --newer <bundle.json> (--log <log.jsonl> | --log-url <url>) --issuer-key <pub> [--log-key <pub>] [--json]
|
|
33
|
+
verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--log-id <id>] [--upstream-key <pub>] [--stripe-secret-env NAME] [--github-secret-env NAME] [--log <log.jsonl>] [--json]
|
|
34
|
+
audit --older <bundle.json> --newer <bundle.json> (--log <log.jsonl> | --log-url <url>) --issuer-key <pub> [--log-key <pub>] [--log-id <id>] [--json]
|
|
34
35
|
checks that the newer receipt's log extends the older one's: nothing between them was rewritten
|
|
35
36
|
`;
|
|
37
|
+
/** The tenants file for `log --tenants`: paths relative to the file, tokens from the environment, ids default to the tenant name. */
|
|
38
|
+
function loadTenants(path) {
|
|
39
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [name, t] of Object.entries(raw)) {
|
|
42
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name) || typeof t?.file !== "string")
|
|
43
|
+
throw new Error(`tenants: "${name}" needs a file, and its name must be a plain identifier`);
|
|
44
|
+
const token = t.tokenEnv ? process.env[t.tokenEnv] : undefined;
|
|
45
|
+
if (t.tokenEnv && !token)
|
|
46
|
+
throw new Error(`tenants: environment variable ${t.tokenEnv} for "${name}" is not set`);
|
|
47
|
+
out[name] = { file: resolve(dirname(resolve(path)), t.file), ...(token ? { tokens: [token] } : {}), ...(t.logId ? { logId: t.logId } : {}) };
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
36
51
|
async function main(argv) {
|
|
37
52
|
const [cmd, ...rest] = argv;
|
|
38
53
|
switch (cmd) {
|
|
@@ -116,7 +131,7 @@ async function main(argv) {
|
|
|
116
131
|
case "log": {
|
|
117
132
|
const { values } = parseArgs({
|
|
118
133
|
args: rest,
|
|
119
|
-
options: { file: { type: "string" }, key: { type: "string" }, port: { type: "string", default: "8787" }, host: { type: "string", default: "127.0.0.1" }, "token-env": { type: "string" } },
|
|
134
|
+
options: { file: { type: "string" }, key: { type: "string" }, port: { type: "string", default: "8787" }, host: { type: "string", default: "127.0.0.1" }, "token-env": { type: "string" }, "log-id": { type: "string" }, tenants: { type: "string" } },
|
|
120
135
|
});
|
|
121
136
|
if (!values.file || !values.key)
|
|
122
137
|
throw new Error("log needs --file and --key");
|
|
@@ -124,8 +139,10 @@ async function main(argv) {
|
|
|
124
139
|
if (values["token-env"] && !token)
|
|
125
140
|
throw new Error(`log: environment variable ${values["token-env"]} is not set`);
|
|
126
141
|
const key = loadPrivateKey(values.key);
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
// --tenants names a JSON file { "<tenant>": { "file": "...", "tokenEnv": "NAME", "logId": "..." } }; each is reached at /t/<tenant>/.
|
|
143
|
+
const tenants = values.tenants ? loadTenants(values.tenants) : undefined;
|
|
144
|
+
const running = await serveLog(values.file, key, { port: Number(values.port), host: values.host, ...(token ? { tokens: [token] } : {}), ...(values["log-id"] ? { logId: values["log-id"] } : {}), ...(tenants ? { tenants } : {}) });
|
|
145
|
+
console.error(`agent-custody log: ${running.url} keyid=${key.keyid} file=${values.file}${values["log-id"] ? ` log=${values["log-id"]}` : ""} ${token ? "bearer token required" : "open, anyone may append"}${tenants ? ` tenants=${Object.keys(tenants).join(",")}` : ""}`);
|
|
129
146
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
130
147
|
await running.close();
|
|
131
148
|
return 0;
|
|
@@ -139,6 +156,7 @@ async function main(argv) {
|
|
|
139
156
|
"gateway-key": { type: "string", multiple: true },
|
|
140
157
|
"principal-key": { type: "string", multiple: true },
|
|
141
158
|
"log-key": { type: "string", multiple: true },
|
|
159
|
+
"log-id": { type: "string" },
|
|
142
160
|
"upstream-key": { type: "string", multiple: true },
|
|
143
161
|
"stripe-secret-env": { type: "string" },
|
|
144
162
|
"github-secret-env": { type: "string" },
|
|
@@ -155,6 +173,7 @@ async function main(argv) {
|
|
|
155
173
|
issuerKeys: issuerKeyFiles.map(loadPublicKey),
|
|
156
174
|
principalKeys: (values["principal-key"] ?? []).map(loadPublicKey),
|
|
157
175
|
...(values["log-key"] ? { logKeys: values["log-key"].map(loadPublicKey) } : {}),
|
|
176
|
+
...(values["log-id"] ? { logId: values["log-id"] } : {}),
|
|
158
177
|
...(values["upstream-key"] ? { upstreamKeys: values["upstream-key"].map(loadPublicKey) } : {}),
|
|
159
178
|
...(values["stripe-secret-env"] || values["github-secret-env"] ? { providerSecrets: { ...(values["stripe-secret-env"] ? { stripe: secretFrom(values["stripe-secret-env"]) } : {}), ...(values["github-secret-env"] ? { github: secretFrom(values["github-secret-env"]) } : {}) } } : {}),
|
|
160
179
|
...(values.log ? { logFile: values.log } : {}),
|
|
@@ -172,6 +191,7 @@ async function main(argv) {
|
|
|
172
191
|
"log-url": { type: "string" },
|
|
173
192
|
"issuer-key": { type: "string", multiple: true },
|
|
174
193
|
"log-key": { type: "string", multiple: true },
|
|
194
|
+
"log-id": { type: "string" },
|
|
175
195
|
json: { type: "boolean", default: false },
|
|
176
196
|
},
|
|
177
197
|
});
|
|
@@ -193,7 +213,7 @@ async function main(argv) {
|
|
|
193
213
|
throw new Error(`log refused the consistency query: ${res.status}`);
|
|
194
214
|
proof = (await res.json()).hashes;
|
|
195
215
|
}
|
|
196
|
-
const result = auditExtends(older, newer, proof, keyFiles.map(loadPublicKey));
|
|
216
|
+
const result = auditExtends(older, newer, proof, keyFiles.map(loadPublicKey), values["log-id"]);
|
|
197
217
|
if (values.json)
|
|
198
218
|
console.log(JSON.stringify(result, null, 2));
|
|
199
219
|
else {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** One REST endpoint offered to the agent as a tool. `{name}` segments in the path come from the call's arguments; the rest go to the query on GET and DELETE, or to a JSON body otherwise. */
|
|
3
|
+
declare const RestToolSchema: z.ZodObject<{
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
method: z.ZodDefault<z.ZodEnum<{
|
|
7
|
+
DELETE: "DELETE";
|
|
8
|
+
GET: "GET";
|
|
9
|
+
PATCH: "PATCH";
|
|
10
|
+
POST: "POST";
|
|
11
|
+
PUT: "PUT";
|
|
12
|
+
}>>;
|
|
13
|
+
path: z.ZodString;
|
|
14
|
+
query: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
body: z.ZodDefault<z.ZodEnum<{
|
|
16
|
+
json: "json";
|
|
17
|
+
none: "none";
|
|
18
|
+
}>>;
|
|
19
|
+
inputSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type RestToolConfig = z.infer<typeof RestToolSchema>;
|
|
22
|
+
/** A plain HTTP API as an upstream: no MCP server needed. Secrets come from the environment through headerEnv, never from the file or the agent. */
|
|
23
|
+
declare const RestUpstreamSchema: z.ZodObject<{
|
|
24
|
+
baseUrl: z.ZodString;
|
|
25
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
26
|
+
headerEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
27
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
28
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
description: z.ZodOptional<z.ZodString>;
|
|
31
|
+
method: z.ZodDefault<z.ZodEnum<{
|
|
32
|
+
DELETE: "DELETE";
|
|
33
|
+
GET: "GET";
|
|
34
|
+
PATCH: "PATCH";
|
|
35
|
+
POST: "POST";
|
|
36
|
+
PUT: "PUT";
|
|
37
|
+
}>>;
|
|
38
|
+
path: z.ZodString;
|
|
39
|
+
query: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
40
|
+
body: z.ZodDefault<z.ZodEnum<{
|
|
41
|
+
json: "json";
|
|
42
|
+
none: "none";
|
|
43
|
+
}>>;
|
|
44
|
+
inputSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type RestUpstreamConfig = z.infer<typeof RestUpstreamSchema>;
|
|
2
48
|
declare const UpstreamSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
3
49
|
command: z.ZodString;
|
|
4
50
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -6,6 +52,31 @@ declare const UpstreamSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
6
52
|
}, z.core.$strip>, z.ZodObject<{
|
|
7
53
|
url: z.ZodString;
|
|
8
54
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
56
|
+
rest: z.ZodObject<{
|
|
57
|
+
baseUrl: z.ZodString;
|
|
58
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
59
|
+
headerEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
60
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
61
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
62
|
+
name: z.ZodString;
|
|
63
|
+
description: z.ZodOptional<z.ZodString>;
|
|
64
|
+
method: z.ZodDefault<z.ZodEnum<{
|
|
65
|
+
DELETE: "DELETE";
|
|
66
|
+
GET: "GET";
|
|
67
|
+
PATCH: "PATCH";
|
|
68
|
+
POST: "POST";
|
|
69
|
+
PUT: "PUT";
|
|
70
|
+
}>>;
|
|
71
|
+
path: z.ZodString;
|
|
72
|
+
query: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
73
|
+
body: z.ZodDefault<z.ZodEnum<{
|
|
74
|
+
json: "json";
|
|
75
|
+
none: "none";
|
|
76
|
+
}>>;
|
|
77
|
+
inputSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
78
|
+
}, z.core.$strip>>;
|
|
79
|
+
}, z.core.$strip>;
|
|
9
80
|
}, z.core.$strip>]>;
|
|
10
81
|
export type UpstreamConfig = z.infer<typeof UpstreamSchema>;
|
|
11
82
|
declare const FactSchema: z.ZodObject<{
|
|
@@ -26,6 +97,31 @@ export declare const GatewayConfigSchema: z.ZodObject<{
|
|
|
26
97
|
}, z.core.$strip>, z.ZodObject<{
|
|
27
98
|
url: z.ZodString;
|
|
28
99
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
101
|
+
rest: z.ZodObject<{
|
|
102
|
+
baseUrl: z.ZodString;
|
|
103
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
104
|
+
headerEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
105
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
106
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
107
|
+
name: z.ZodString;
|
|
108
|
+
description: z.ZodOptional<z.ZodString>;
|
|
109
|
+
method: z.ZodDefault<z.ZodEnum<{
|
|
110
|
+
DELETE: "DELETE";
|
|
111
|
+
GET: "GET";
|
|
112
|
+
PATCH: "PATCH";
|
|
113
|
+
POST: "POST";
|
|
114
|
+
PUT: "PUT";
|
|
115
|
+
}>>;
|
|
116
|
+
path: z.ZodString;
|
|
117
|
+
query: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
118
|
+
body: z.ZodDefault<z.ZodEnum<{
|
|
119
|
+
json: "json";
|
|
120
|
+
none: "none";
|
|
121
|
+
}>>;
|
|
122
|
+
inputSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
123
|
+
}, z.core.$strip>>;
|
|
124
|
+
}, z.core.$strip>;
|
|
29
125
|
}, z.core.$strip>]>>;
|
|
30
126
|
upstreams: z.ZodOptional<z.ZodArray<z.ZodIntersection<z.ZodUnion<readonly [z.ZodObject<{
|
|
31
127
|
command: z.ZodString;
|
|
@@ -34,6 +130,31 @@ export declare const GatewayConfigSchema: z.ZodObject<{
|
|
|
34
130
|
}, z.core.$strip>, z.ZodObject<{
|
|
35
131
|
url: z.ZodString;
|
|
36
132
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
133
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
134
|
+
rest: z.ZodObject<{
|
|
135
|
+
baseUrl: z.ZodString;
|
|
136
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
137
|
+
headerEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
138
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
139
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
140
|
+
name: z.ZodString;
|
|
141
|
+
description: z.ZodOptional<z.ZodString>;
|
|
142
|
+
method: z.ZodDefault<z.ZodEnum<{
|
|
143
|
+
DELETE: "DELETE";
|
|
144
|
+
GET: "GET";
|
|
145
|
+
PATCH: "PATCH";
|
|
146
|
+
POST: "POST";
|
|
147
|
+
PUT: "PUT";
|
|
148
|
+
}>>;
|
|
149
|
+
path: z.ZodString;
|
|
150
|
+
query: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
151
|
+
body: z.ZodDefault<z.ZodEnum<{
|
|
152
|
+
json: "json";
|
|
153
|
+
none: "none";
|
|
154
|
+
}>>;
|
|
155
|
+
inputSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
}, z.core.$strip>;
|
|
37
158
|
}, z.core.$strip>]>, z.ZodObject<{
|
|
38
159
|
name: z.ZodString;
|
|
39
160
|
}, z.core.$strip>>>>;
|
|
@@ -47,11 +168,18 @@ export declare const GatewayConfigSchema: z.ZodObject<{
|
|
|
47
168
|
forTools: z.ZodArray<z.ZodString>;
|
|
48
169
|
optional: z.ZodDefault<z.ZodBoolean>;
|
|
49
170
|
}, z.core.$strip>>>;
|
|
171
|
+
precommit: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
50
172
|
receiptsDir: z.ZodString;
|
|
51
173
|
logFile: z.ZodOptional<z.ZodString>;
|
|
52
174
|
log: z.ZodOptional<z.ZodObject<{
|
|
53
175
|
url: z.ZodString;
|
|
54
176
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
177
|
+
hashOnly: z.ZodOptional<z.ZodBoolean>;
|
|
178
|
+
}, z.core.$strip>>;
|
|
179
|
+
otel: z.ZodOptional<z.ZodObject<{
|
|
180
|
+
url: z.ZodString;
|
|
181
|
+
headersEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
182
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
55
183
|
}, z.core.$strip>>;
|
|
56
184
|
}, z.core.$strip>;
|
|
57
185
|
export type GatewayConfig = z.infer<typeof GatewayConfigSchema>;
|
|
@@ -70,6 +198,12 @@ export declare const SdkConfigSchema: z.ZodObject<{
|
|
|
70
198
|
log: z.ZodOptional<z.ZodObject<{
|
|
71
199
|
url: z.ZodString;
|
|
72
200
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
201
|
+
hashOnly: z.ZodOptional<z.ZodBoolean>;
|
|
202
|
+
}, z.core.$strip>>;
|
|
203
|
+
otel: z.ZodOptional<z.ZodObject<{
|
|
204
|
+
url: z.ZodString;
|
|
205
|
+
headersEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
206
|
+
serviceName: z.ZodOptional<z.ZodString>;
|
|
73
207
|
}, z.core.$strip>>;
|
|
74
208
|
framework: z.ZodOptional<z.ZodString>;
|
|
75
209
|
}, z.core.$strip>;
|
package/dist/config.js
CHANGED
|
@@ -2,12 +2,37 @@ import { z } from "zod";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
/** Where receipts are logged: a local file, or a log reached over HTTP whose bearer token comes from an environment variable. */
|
|
5
|
-
const LogSchema = z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional() });
|
|
5
|
+
const LogSchema = z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional(), /** send leaf hashes only; the log never holds the receipt. Use it for any log run by someone else */ hashOnly: z.boolean().optional() });
|
|
6
6
|
const oneLog = { message: "exactly one of logFile or log is required" };
|
|
7
|
+
/** Optional OpenTelemetry export: every receipt also becomes a span at this OTLP/HTTP collector, after it is issued. Never on the evidence path. */
|
|
8
|
+
const OtelSchema = z.object({ url: z.string().url(), headersEnv: z.record(z.string(), z.string().min(1)).optional(), serviceName: z.string().min(1).optional() });
|
|
7
9
|
const hasOneLog = (c) => (c.logFile ? 1 : 0) + (c.log ? 1 : 0) === 1;
|
|
10
|
+
/** One REST endpoint offered to the agent as a tool. `{name}` segments in the path come from the call's arguments; the rest go to the query on GET and DELETE, or to a JSON body otherwise. */
|
|
11
|
+
const RestToolSchema = z.object({
|
|
12
|
+
name: z.string().min(1),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
|
|
15
|
+
path: z.string().min(1),
|
|
16
|
+
/** argument names sent as query parameters, when the default placement is not wanted */
|
|
17
|
+
query: z.array(z.string().min(1)).optional(),
|
|
18
|
+
/** "none" sends no body even on POST */
|
|
19
|
+
body: z.enum(["json", "none"]).default("json"),
|
|
20
|
+
/** the JSON Schema the agent sees; default accepts any object */
|
|
21
|
+
inputSchema: z.record(z.string(), z.unknown()).default({ type: "object" }),
|
|
22
|
+
});
|
|
23
|
+
/** A plain HTTP API as an upstream: no MCP server needed. Secrets come from the environment through headerEnv, never from the file or the agent. */
|
|
24
|
+
const RestUpstreamSchema = z.object({
|
|
25
|
+
baseUrl: z.string().url(),
|
|
26
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
27
|
+
/** header name to environment variable, e.g. { "authorization": "STRIPE_BEARER" }; a missing variable fails at startup */
|
|
28
|
+
headerEnv: z.record(z.string(), z.string().min(1)).optional(),
|
|
29
|
+
timeoutMs: z.number().int().positive().default(30_000),
|
|
30
|
+
tools: z.array(RestToolSchema).min(1),
|
|
31
|
+
});
|
|
8
32
|
const UpstreamSchema = z.union([
|
|
9
33
|
z.object({ command: z.string(), args: z.array(z.string()).default([]), env: z.record(z.string(), z.string()).optional() }),
|
|
10
34
|
z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional() }),
|
|
35
|
+
z.object({ rest: RestUpstreamSchema }),
|
|
11
36
|
]);
|
|
12
37
|
const FactSchema = z.object({
|
|
13
38
|
/** key under context.facts */
|
|
@@ -23,7 +48,7 @@ const FactSchema = z.object({
|
|
|
23
48
|
});
|
|
24
49
|
export const GatewayConfigSchema = z.object({
|
|
25
50
|
identity: z.object({ keyFile: z.string() }),
|
|
26
|
-
/** the upstream MCP server
|
|
51
|
+
/** the upstream: an MCP server to spawn over stdio, an MCP URL to reach over Streamable HTTP with an optional bearer token from the environment, or a REST API described as tools */
|
|
27
52
|
upstream: UpstreamSchema.optional(),
|
|
28
53
|
/** several upstreams behind one gateway and one grant; each tool name must belong to exactly one of them */
|
|
29
54
|
upstreams: z.array(UpstreamSchema.and(z.object({ name: z.string().min(1) }))).min(1).optional(),
|
|
@@ -31,9 +56,15 @@ export const GatewayConfigSchema = z.object({
|
|
|
31
56
|
trustedPrincipalKeys: z.array(z.string()).min(1),
|
|
32
57
|
policyFile: z.string(),
|
|
33
58
|
facts: z.array(FactSchema).default([]),
|
|
59
|
+
/**
|
|
60
|
+
* Consequential tools, by name or "*" for all: before forwarding one of these, the gateway commits a signed
|
|
61
|
+
* authorization to the log and refuses the call if the log will not take it. Evidence then precedes the side effect.
|
|
62
|
+
*/
|
|
63
|
+
precommit: z.array(z.string().min(1)).default([]),
|
|
34
64
|
receiptsDir: z.string(),
|
|
35
65
|
logFile: z.string().optional(),
|
|
36
66
|
log: LogSchema.optional(),
|
|
67
|
+
otel: OtelSchema.optional(),
|
|
37
68
|
}).refine(hasOneLog, oneLog).refine((c) => (c.upstream ? 1 : 0) + (c.upstreams ? 1 : 0) === 1, { message: "exactly one of upstream or upstreams is required" });
|
|
38
69
|
/** Loads a config file and resolves every path relative to the file's directory. */
|
|
39
70
|
export function loadConfig(path) {
|
|
@@ -60,6 +91,7 @@ export const SdkConfigSchema = z.object({
|
|
|
60
91
|
receiptsDir: z.string(),
|
|
61
92
|
logFile: z.string().optional(),
|
|
62
93
|
log: LogSchema.optional(),
|
|
94
|
+
otel: OtelSchema.optional(),
|
|
63
95
|
/** free-text label of the host framework, e.g. "claude-code", "openai-agents" */
|
|
64
96
|
framework: z.string().optional(),
|
|
65
97
|
}).refine(hasOneLog, oneLog);
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { GatewayConfig } from "./config.ts";
|
|
3
3
|
import { type Delegation } from "./delegation.ts";
|
|
4
|
+
import { type LogSink } from "./log-sink.ts";
|
|
5
|
+
import { type ReceiptExporter } from "./otel.ts";
|
|
4
6
|
export declare const GATEWAY_VERSION = "0.1.0";
|
|
5
7
|
export declare const RECEIPT_META_KEY = "agent-custody/receipt";
|
|
6
8
|
export declare const MODEL_META_KEY = "agent-custody/model";
|
|
@@ -23,6 +25,12 @@ export interface Gateway {
|
|
|
23
25
|
handleCall(params: CallParams): Promise<CallToolResult>;
|
|
24
26
|
close(): Promise<void>;
|
|
25
27
|
}
|
|
26
|
-
export
|
|
28
|
+
export interface GatewayOptions {
|
|
29
|
+
/** the log to append to, in place of the one the config names; for embedding and tests */
|
|
30
|
+
log?: LogSink;
|
|
31
|
+
/** told about every receipt after it is written, in place of the exporter the config names */
|
|
32
|
+
exporter?: ReceiptExporter;
|
|
33
|
+
}
|
|
34
|
+
export declare function createGateway(cfg: GatewayConfig, options?: GatewayOptions): Promise<Gateway>;
|
|
27
35
|
/** Exposes the gateway as an MCP server over stdio. Everything diagnostic must go to stderr. */
|
|
28
36
|
export declare function serveStdio(gw: Gateway): Promise<void>;
|
package/dist/gateway.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The MCP gateway: sits between an agent and
|
|
1
|
+
// The MCP gateway: sits between an agent and its upstreams, MCP servers or REST APIs, enforces scope + Cedar policy,
|
|
2
2
|
// and emits a signed, logged receipt for every tool call, allowed or denied.
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
@@ -12,7 +12,9 @@ import { digestOf, loadPrivateKey, loadPublicKey } from "./crypto.js";
|
|
|
12
12
|
import { delegationValidAt, verifyDelegation } from "./delegation.js";
|
|
13
13
|
import { createIssuer } from "./issue.js";
|
|
14
14
|
import { openLog } from "./log-sink.js";
|
|
15
|
+
import { openExporter } from "./otel.js";
|
|
15
16
|
import { upstreamEvidenceOf } from "./upstream.js";
|
|
17
|
+
import { restUpstream } from "./rest.js";
|
|
16
18
|
import { evaluate, policyDigest } from "./policy.js";
|
|
17
19
|
export const GATEWAY_VERSION = "0.1.0";
|
|
18
20
|
export const RECEIPT_META_KEY = "agent-custody/receipt";
|
|
@@ -55,7 +57,7 @@ function extractValue(result) {
|
|
|
55
57
|
return text.text;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
export async function createGateway(cfg) {
|
|
60
|
+
export async function createGateway(cfg, options = {}) {
|
|
59
61
|
const gatewayKey = loadPrivateKey(cfg.identity.keyFile);
|
|
60
62
|
const trusted = cfg.trustedPrincipalKeys.map(loadPublicKey);
|
|
61
63
|
const grantEnvelope = JSON.parse(readFileSync(cfg.grantFile, "utf8"));
|
|
@@ -68,7 +70,9 @@ export async function createGateway(cfg) {
|
|
|
68
70
|
const principalKeyid = grant.keyid;
|
|
69
71
|
const policyText = readFileSync(cfg.policyFile, "utf8");
|
|
70
72
|
const pDigest = policyDigest(policyText);
|
|
71
|
-
const issuer = createIssuer(gatewayKey, cfg.receiptsDir, openLog(cfg, gatewayKey));
|
|
73
|
+
const issuer = createIssuer(gatewayKey, cfg.receiptsDir, options.log ?? openLog(cfg, gatewayKey), { exporter: options.exporter ?? openExporter(cfg) });
|
|
74
|
+
const precommit = new Set(cfg.precommit);
|
|
75
|
+
const consequential = (tool) => precommit.has("*") || precommit.has(tool);
|
|
72
76
|
// One gateway, one grant, one session, and as many upstreams as the agent's job needs. Each tool name belongs to
|
|
73
77
|
// exactly one upstream, decided at startup, so a receipt's tool is unambiguous and consumed facts flow across them.
|
|
74
78
|
const upstreamConfigs = cfg.upstreams ? cfg.upstreams.map((u) => ({ name: u.name, cfg: u })) : [{ name: "upstream", cfg: cfg.upstream }];
|
|
@@ -76,14 +80,19 @@ export async function createGateway(cfg) {
|
|
|
76
80
|
const owner = new Map();
|
|
77
81
|
const advertised = [];
|
|
78
82
|
for (const { name, cfg: u } of upstreamConfigs) {
|
|
79
|
-
|
|
80
|
-
if ("
|
|
83
|
+
let client;
|
|
84
|
+
if ("rest" in u) {
|
|
85
|
+
client = restUpstream(name, u.rest);
|
|
86
|
+
}
|
|
87
|
+
else if ("url" in u) {
|
|
88
|
+
client = new Client({ name: "agent-custody-gateway", version: GATEWAY_VERSION });
|
|
81
89
|
const token = u.tokenEnv ? process.env[u.tokenEnv] : undefined;
|
|
82
90
|
if (u.tokenEnv && !token)
|
|
83
91
|
throw new Error(`upstream ${name}: environment variable ${u.tokenEnv} is not set`);
|
|
84
92
|
await client.connect(new StreamableHTTPClientTransport(new URL(u.url), token ? { requestInit: { headers: { authorization: `Bearer ${token}` } } } : {}));
|
|
85
93
|
}
|
|
86
94
|
else {
|
|
95
|
+
client = new Client({ name: "agent-custody-gateway", version: GATEWAY_VERSION });
|
|
87
96
|
await client.connect(new StdioClientTransport({ command: u.command, args: u.args, env: u.env, stderr: "inherit" }));
|
|
88
97
|
}
|
|
89
98
|
upstreams.set(name, client);
|
|
@@ -141,6 +150,7 @@ export async function createGateway(cfg) {
|
|
|
141
150
|
let facts = {};
|
|
142
151
|
let policy;
|
|
143
152
|
let execution;
|
|
153
|
+
let authorization;
|
|
144
154
|
if (!delegation.scopes.includes(tool)) {
|
|
145
155
|
policy = { decision: "deny", reasons: [], errors: [`tool "${tool}" is not in the delegation scopes`], policyDigest: pDigest };
|
|
146
156
|
}
|
|
@@ -158,7 +168,32 @@ export async function createGateway(cfg) {
|
|
|
158
168
|
policy = { decision: "deny", reasons: [], errors: [String(e instanceof Error ? e.message : e)], policyDigest: pDigest };
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
|
-
|
|
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
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (execution) {
|
|
194
|
+
// withheld: nothing was forwarded
|
|
195
|
+
}
|
|
196
|
+
else if (policy.decision === "allow") {
|
|
162
197
|
try {
|
|
163
198
|
// The upstream learns which receipt this call is, and who the grant says is calling. An upstream that keeps
|
|
164
199
|
// state, such as the memory server, cites the receipt as the source of what it stores.
|
|
@@ -176,19 +211,11 @@ export async function createGateway(cfg) {
|
|
|
176
211
|
execution = { status: "denied", reason: [...policy.reasons, ...policy.errors].join("; ") || "no permit policy matched", provenance: "observed" };
|
|
177
212
|
}
|
|
178
213
|
await issuer.issue({
|
|
179
|
-
|
|
180
|
-
timestamp,
|
|
181
|
-
issuer: { kind: "gateway", keyid: issuer.keyid, version: GATEWAY_VERSION },
|
|
182
|
-
principal: { id: delegation.principal, keyid: principalKeyid, provenance: "attested" },
|
|
183
|
-
agent: { id: delegation.agent, provenance: "attested" },
|
|
184
|
-
delegation: { envelope: grantEnvelope, provenance: "attested" },
|
|
214
|
+
...head,
|
|
185
215
|
session: { id: null, toolUseId: null, provenance: "claimed" },
|
|
186
216
|
model: { id: typeof modelClaim === "string" ? modelClaim : null, provenance: "claimed" },
|
|
187
|
-
tool: { name: tool, provenance: "observed", ...(owner.has(tool) && upstreamConfigs.length > 1 ? { upstream: owner.get(tool) } : {}) },
|
|
188
|
-
request: { args, argsDigest: digestOf(args), provenance: "claimed" },
|
|
189
|
-
facts,
|
|
190
|
-
consumed: { factIds: consumedNow, provenance: "observed" },
|
|
191
217
|
policy: { ...policy, provenance: "observed" },
|
|
218
|
+
...(authorization ? { authorization } : {}),
|
|
192
219
|
execution,
|
|
193
220
|
});
|
|
194
221
|
const meta = { [RECEIPT_META_KEY]: receiptId };
|
|
@@ -198,6 +225,8 @@ export async function createGateway(cfg) {
|
|
|
198
225
|
return refuse(`Denied by policy: ${execution.reason}`);
|
|
199
226
|
case "error":
|
|
200
227
|
return refuse(`Upstream error: ${execution.error}`);
|
|
228
|
+
case "withheld":
|
|
229
|
+
return refuse(`Not executed: ${execution.reason}`);
|
|
201
230
|
default: {
|
|
202
231
|
const result = execution.result;
|
|
203
232
|
return { ...result, _meta: { ...result._meta, ...meta } };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export { AUTHORIZATION_PREDICATE_TYPE, buildAuthorizationStatement } from "./receipt.ts";
|
|
2
|
+
export type { AuthorizationBundle, AuthorizationPredicate, AuthorizationStatement } from "./receipt.ts";
|
|
3
|
+
export type { GatewayOptions } from "./gateway.ts";
|
|
4
|
+
export { buildRequest, restUpstream } from "./rest.ts";
|
|
5
|
+
export { openExporter, otlpExporter, spanFor } from "./otel.ts";
|
|
6
|
+
export type { OtelConfig, OtlpOptions, ReceiptExporter } from "./otel.ts";
|
|
7
|
+
export type { IssuerOptions } from "./issue.ts";
|
|
8
|
+
export type { RestOptions, UpstreamClient } from "./rest.ts";
|
|
1
9
|
export * from "./config.ts";
|
|
2
10
|
export * from "./crypto.ts";
|
|
3
11
|
export * from "./delegation.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// Public surface of @agent-custody/receipts. Framework adapters live on subpaths, ./sdk/<framework>, because they import optional peers.
|
|
2
|
+
export { AUTHORIZATION_PREDICATE_TYPE, buildAuthorizationStatement } from "./receipt.js";
|
|
3
|
+
export { buildRequest, restUpstream } from "./rest.js";
|
|
4
|
+
export { openExporter, otlpExporter, spanFor } from "./otel.js";
|
|
2
5
|
export * from "./config.js";
|
|
3
6
|
export * from "./crypto.js";
|
|
4
7
|
export * from "./delegation.js";
|