@agent-custody/receipts 0.5.8 → 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/checkpoints.js +1 -1
- package/dist/cli.js +44 -8
- 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/log-sink.d.ts +7 -2
- package/dist/log-sink.js +9 -3
- package/dist/verify.js +6 -1
- package/dist/witness.js +7 -2
- package/docs/tutorials.md +1 -0
- package/docs/usage.md +7 -1
- 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/README.md
CHANGED
|
@@ -238,7 +238,8 @@ src/policy.ts Cedar evaluation wrapper, fail-closed
|
|
|
238
238
|
src/delegation.ts signed delegation grants
|
|
239
239
|
src/receipt.ts receipt and authorization statement types and provenance labels
|
|
240
240
|
src/issue.ts sign, log, and write a receipt, or commit an authorization first; shared by both producers
|
|
241
|
-
src/gateway.ts the MCP proxy: scope check, facts, policy, forward, receipt
|
|
241
|
+
src/gateway.ts the MCP proxy: a host (key, policy, upstreams, log) and a session per grant; scope check, facts, policy, forward, receipt
|
|
242
|
+
src/gateway-http.ts the gateway over Streamable HTTP: one process, a session per connection, each under the grant it presents
|
|
242
243
|
src/sdk/index.ts the interceptor: policy decision, record, wrap(tool fn)
|
|
243
244
|
src/sdk/claude.ts Claude Code command hook and Claude Agent SDK in-process hooks
|
|
244
245
|
src/sdk/openai-agents.ts, vercel-ai.ts, langchain.ts framework adapters, tested against the real packages
|
|
@@ -294,6 +295,8 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
|
|
|
294
295
|
- The log over Postgres: `log --db-env`, leaves as hashes in one table keyed by tenant, one writer per tenant by advisory lock, tenants and hashed tokens in tables managed by `log-admin`, rate limits and a body cap, retries in the sink, and `import` for an existing file log. Phase 2 of [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
|
|
295
296
|
- 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).
|
|
296
297
|
- 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.
|
|
298
|
+
- Delegation chains for sub-agents: a grant that names the agent's key lets it delegate a narrower grant, with the parent embedded, up to three deep; the verifier and the gateway walk the chain to the principal, refusing any link that escalates scope, widens the window, changes the principal, or is signed by the wrong key; the receipt names the sub-agent and the principal and carries the chain. Pinned by the `gateway-chain-*` vectors and mirrored in the browser verifier.
|
|
299
|
+
- One gateway for many agents: `gateway --http` serves the gateway over Streamable HTTP, one session per connection under the grant that connection presents, sessions sharing the upstreams and the policy and nothing else; a platform team runs one gateway in front of the tools instead of one process per agent.
|
|
297
300
|
- Splunk export: with `splunk` in either config, every receipt is also one event at the HTTP Event Collector, with the receipt id, tool, agent, principal, status, decision, and log position as searchable fields and the token from the environment; the same best-effort rule.
|
|
298
301
|
- 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.
|
|
299
302
|
- 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.
|
|
@@ -302,9 +305,7 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
|
|
|
302
305
|
|
|
303
306
|
1. Run the witness for log.agent-custody.dev on a machine and under an account that is not ours, and require it in the welcome sheet. The code is done; what it needs is a second operator. [Issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
|
|
304
307
|
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).
|
|
305
|
-
3.
|
|
306
|
-
4.
|
|
307
|
-
5. Receiver-attested receipts for agent-to-agent calls.
|
|
308
|
-
6. A TEE-hosted signer, then SD-JWT redaction, then ZK proofs of policy compliance. Not before.
|
|
308
|
+
3. Receiver-attested receipts for agent-to-agent calls.
|
|
309
|
+
4. A TEE-hosted signer, then SD-JWT redaction, then ZK proofs of policy compliance. Not before.
|
|
309
310
|
|
|
310
311
|
A Python SDK follows the same shape once the TypeScript adapters have settled.
|
package/dist/checkpoints.js
CHANGED
|
@@ -55,7 +55,7 @@ export function postgresCheckpoints(client, prefix = "log_") {
|
|
|
55
55
|
return {
|
|
56
56
|
async save(c) {
|
|
57
57
|
await init();
|
|
58
|
-
await client.query(`INSERT INTO ${table} (tenant_id, tree_size, log_id, root_hash, signed_at, envelope) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (tenant_id, tree_size) DO
|
|
58
|
+
await client.query(`INSERT INTO ${table} (tenant_id, tree_size, log_id, root_hash, signed_at, envelope) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (tenant_id, tree_size) DO UPDATE SET signed_at = EXCLUDED.signed_at, envelope = EXCLUDED.envelope WHERE ${table}.root_hash = EXCLUDED.root_hash`, [c.tenant, c.treeSize, c.logId ?? null, c.rootHash, c.signedAt, JSON.stringify(c.envelope)]);
|
|
59
59
|
},
|
|
60
60
|
async list(tenant, since = -1) {
|
|
61
61
|
await init();
|
package/dist/cli.js
CHANGED
|
@@ -5,14 +5,15 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { loadConfig, loadSdkConfig } from "./config.js";
|
|
7
7
|
import { generateKeyPair, loadPrivateKey, loadPublicKey, writeKeyPair } from "./crypto.js";
|
|
8
|
-
import { createDelegation } from "./delegation.js";
|
|
9
|
-
import { createGateway, serveStdio } from "./gateway.js";
|
|
8
|
+
import { createDelegation, decodeDelegation, delegateFrom } from "./delegation.js";
|
|
9
|
+
import { createGateway, createGatewayHost, serveStdio } from "./gateway.js";
|
|
10
10
|
import { postgresResolver, serveLog } from "./log-sink.js";
|
|
11
11
|
import { importLogFile, PostgresTenancy } from "./log-store.js";
|
|
12
12
|
import { bothCheckpoints, dirCheckpoints, postgresCheckpoints } from "./checkpoints.js";
|
|
13
13
|
import { connectSigner, fetchLogKeys, localSigner, serveSigner } from "./signer.js";
|
|
14
14
|
import { fetchWitnessKeys, Witness } from "./witness.js";
|
|
15
15
|
import { checkLog, formatLogCheck } from "./log-check.js";
|
|
16
|
+
import { serveHttp } from "./gateway-http.js";
|
|
16
17
|
import { exportLog, formatExport } from "./log-export.js";
|
|
17
18
|
import { CheckpointPublisher, fileResolver } from "./log-sink.js";
|
|
18
19
|
import { createRequire } from "node:module";
|
|
@@ -33,8 +34,13 @@ function secretFrom(envName) {
|
|
|
33
34
|
const USAGE = `agent-custody <command>
|
|
34
35
|
|
|
35
36
|
keygen --dir <dir> --name <name>
|
|
36
|
-
grant --key <principal.key> --principal <id> --agent <id> --scopes <a,b> [--ttl-hours 24] --out <file>
|
|
37
|
-
|
|
37
|
+
grant --key <principal.key> --principal <id> --agent <id> --scopes <a,b> [--ttl-hours 24] [--agent-key <agent.pub>] --out <file>
|
|
38
|
+
--agent-key names the agent's own key in the grant, so the agent may delegate
|
|
39
|
+
delegate --key <agent.key> --parent <grant.json> --agent <sub-agent> --scopes <a,b> [--ttl-hours N] [--agent-key <sub.pub>] --out <file>
|
|
40
|
+
a narrower grant for a sub-agent, signed by the agent the parent names; the parent travels inside
|
|
41
|
+
gateway --config <gateway.json> [--http [--port 8790] [--host 127.0.0.1] [--idle-minutes 30]]
|
|
42
|
+
stdio: one gateway for the grant the config names. --http: one shared gateway, MCP over
|
|
43
|
+
Streamable HTTP at /mcp, each connection presenting its own grant as Authorization: Bearer
|
|
38
44
|
hook [--config <sdk.json>] Claude Code hook command; reads the event on stdin (or AGENT_CUSTODY_CONFIG)
|
|
39
45
|
serve --config <sdk.json> [--port 8788] [--host 127.0.0.1] the SDK as a local HTTP API for agents in other languages
|
|
40
46
|
prune --log <log.jsonl> --before <ISO instant> [--receipts <dir>]
|
|
@@ -43,7 +49,7 @@ const USAGE = `agent-custody <command>
|
|
|
43
49
|
verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub> | --log-url <url>] [--log-id <id>] [--upstream-key <pub>] [--stripe-secret-env NAME] [--github-secret-env NAME] [--log <log.jsonl>] [--json]
|
|
44
50
|
log ... --db-env NAME the same server over Postgres: tenants and tokens from the database, one writer per tenant,
|
|
45
51
|
root paths serve the tenant "default" (created with --log-id). Needs the pg package.
|
|
46
|
-
log ... (--key <log.key> [--retired-key <pub>]... | --signer-url <url> [--signer-token-env NAME]) [--checkpoint-dir <dir>] [--checkpoint-every <seconds>]
|
|
52
|
+
log ... (--key <log.key> [--retired-key <pub>]... | --signer-url <url> [--signer-token-env NAME]) [--checkpoint-dir <dir>] [--checkpoint-every <seconds>] [--checkpoint-heartbeat <seconds>]
|
|
47
53
|
sign with a key in this process, or through a signer process that holds it; publish a signed
|
|
48
54
|
checkpoint per log that has grown, every 300 s by default, to the directory (and, with a
|
|
49
55
|
database, to its heads table); serve the key document at /.well-known/agent-custody-log.json
|
|
@@ -126,6 +132,7 @@ async function main(argv) {
|
|
|
126
132
|
agent: { type: "string" },
|
|
127
133
|
scopes: { type: "string" },
|
|
128
134
|
"ttl-hours": { type: "string", default: "24" },
|
|
135
|
+
"agent-key": { type: "string" },
|
|
129
136
|
out: { type: "string" },
|
|
130
137
|
},
|
|
131
138
|
});
|
|
@@ -139,15 +146,41 @@ async function main(argv) {
|
|
|
139
146
|
scopes: values.scopes.split(",").map((s) => s.trim()).filter(Boolean),
|
|
140
147
|
issuedAt: new Date(now).toISOString(),
|
|
141
148
|
expiresAt: new Date(now + Number(values["ttl-hours"]) * 3600_000).toISOString(),
|
|
149
|
+
...(values["agent-key"] ? { agentKey: readFileSync(values["agent-key"], "utf8") } : {}),
|
|
142
150
|
});
|
|
143
151
|
writeFileSync(values.out, JSON.stringify(env, null, 2));
|
|
144
152
|
console.log(`wrote ${values.out}`);
|
|
145
153
|
return 0;
|
|
146
154
|
}
|
|
155
|
+
case "delegate": {
|
|
156
|
+
const { values } = parseArgs({ args: rest, options: { key: { type: "string" }, parent: { type: "string" }, agent: { type: "string" }, scopes: { type: "string" }, "ttl-hours": { type: "string" }, "agent-key": { type: "string" }, out: { type: "string" } } });
|
|
157
|
+
if (!values.key || !values.parent || !values.agent || !values.scopes || !values.out)
|
|
158
|
+
throw new Error("delegate needs --key --parent --agent --scopes --out");
|
|
159
|
+
const parent = JSON.parse(readFileSync(values.parent, "utf8"));
|
|
160
|
+
const env = delegateFrom(parent, loadPrivateKey(values.key), {
|
|
161
|
+
agent: values.agent,
|
|
162
|
+
scopes: values.scopes.split(",").map((s) => s.trim()).filter(Boolean),
|
|
163
|
+
...(values["ttl-hours"] ? { expiresAt: new Date(Date.now() + Number(values["ttl-hours"]) * 3600_000).toISOString() } : {}),
|
|
164
|
+
...(values["agent-key"] ? { agentKey: readFileSync(values["agent-key"], "utf8") } : {}),
|
|
165
|
+
});
|
|
166
|
+
writeFileSync(values.out, JSON.stringify(env, null, 2));
|
|
167
|
+
const chain = decodeDelegation(env);
|
|
168
|
+
console.log(`delegated to ${chain.agent}: scopes [${chain.scopes.join(", ")}] until ${chain.expiresAt}, under ${decodeDelegation(parent).agent}'s grant from ${chain.principal}`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
147
171
|
case "gateway": {
|
|
148
|
-
const { values } = parseArgs({ args: rest, options: { config: { type: "string" } } });
|
|
172
|
+
const { values } = parseArgs({ args: rest, options: { config: { type: "string" }, http: { type: "boolean", default: false }, port: { type: "string", default: "8790" }, host: { type: "string", default: "127.0.0.1" }, "idle-minutes": { type: "string", default: "30" } } });
|
|
149
173
|
if (!values.config)
|
|
150
174
|
throw new Error("gateway needs --config");
|
|
175
|
+
if (values.http) {
|
|
176
|
+
const host = await createGatewayHost(loadConfig(values.config));
|
|
177
|
+
const running = await serveHttp(host, { port: Number(values.port), host: values.host, idleMs: Number(values["idle-minutes"]) * 60_000 });
|
|
178
|
+
console.error(`agent-custody gateway: ${running.url} keyid=${host.keyid} one session per grant; GET /health`);
|
|
179
|
+
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
180
|
+
await running.close();
|
|
181
|
+
await host.close();
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
151
184
|
const gw = await createGateway(loadConfig(values.config));
|
|
152
185
|
console.error(`agent-custody gateway: agent=${gw.agentId} principal=${gw.delegation.principal} scopes=[${gw.delegation.scopes.join(", ")}]`);
|
|
153
186
|
await serveStdio(gw);
|
|
@@ -292,7 +325,7 @@ async function main(argv) {
|
|
|
292
325
|
case "log": {
|
|
293
326
|
const { values } = parseArgs({
|
|
294
327
|
args: rest,
|
|
295
|
-
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" }, "db-env": { type: "string" }, "signer-url": { type: "string" }, "signer-token-env": { type: "string" }, "retired-key": { type: "string", multiple: true }, "checkpoint-dir": { type: "string" }, "checkpoint-every": { type: "string", default: "300" }, "admin-token-env": { type: "string" }, "public-url": { type: "string" }, "checkpoints-url": { type: "string" }, "trust-proxy": { type: "boolean", default: false } },
|
|
328
|
+
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" }, "db-env": { type: "string" }, "signer-url": { type: "string" }, "signer-token-env": { type: "string" }, "retired-key": { type: "string", multiple: true }, "checkpoint-dir": { type: "string" }, "checkpoint-every": { type: "string", default: "300" }, "checkpoint-heartbeat": { type: "string", default: "21600" }, "admin-token-env": { type: "string" }, "public-url": { type: "string" }, "checkpoints-url": { type: "string" }, "trust-proxy": { type: "boolean", default: false } },
|
|
296
329
|
});
|
|
297
330
|
if (!values.key === !values["signer-url"])
|
|
298
331
|
throw new Error("log needs exactly one of --key or --signer-url");
|
|
@@ -312,6 +345,9 @@ async function main(argv) {
|
|
|
312
345
|
const everyMs = Number(values["checkpoint-every"]) * 1000;
|
|
313
346
|
if (!(everyMs > 0))
|
|
314
347
|
throw new Error("--checkpoint-every must be a positive number of seconds");
|
|
348
|
+
const heartbeatMs = Number(values["checkpoint-heartbeat"]) * 1000;
|
|
349
|
+
if (!(heartbeatMs > 0))
|
|
350
|
+
throw new Error("--checkpoint-heartbeat must be a positive number of seconds");
|
|
315
351
|
let resolver;
|
|
316
352
|
let checkpoints = values["checkpoint-dir"] ? dirCheckpoints(values["checkpoint-dir"]) : undefined;
|
|
317
353
|
let where;
|
|
@@ -345,7 +381,7 @@ async function main(argv) {
|
|
|
345
381
|
where = `file=${values.file}${values["log-id"] ? ` log=${values["log-id"]}` : ""} ${token ? "bearer token required" : "open, anyone may append"}${tenants ? ` tenants=${Object.keys(tenants).join(",")}` : ""}`;
|
|
346
382
|
}
|
|
347
383
|
const running = await serveLog(resolver, signer, { port: Number(values.port), host: values.host, ...(checkpoints ? { checkpoints } : {}), ...(admin ? { admin } : {}), trustProxy: values["trust-proxy"] });
|
|
348
|
-
const publisher = checkpoints ? new CheckpointPublisher(resolver, signer, checkpoints, everyMs) : null;
|
|
384
|
+
const publisher = checkpoints ? new CheckpointPublisher(resolver, signer, checkpoints, everyMs, undefined, heartbeatMs) : null;
|
|
349
385
|
publisher?.start();
|
|
350
386
|
console.error(`agent-custody log: ${running.url} keyid=${signer.keyid} ${values["signer-url"] ? `signer=${values["signer-url"]} ` : ""}${where}${checkpoints ? ` checkpoints every ${values["checkpoint-every"]}s${values["checkpoint-dir"] ? ` to ${values["checkpoint-dir"]}` : ""}` : ""}${admin ? " admin page at /admin" : ""}`);
|
|
351
387
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
package/dist/config.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export declare const GatewayConfigSchema: z.ZodObject<{
|
|
|
158
158
|
}, z.core.$strip>]>, z.ZodObject<{
|
|
159
159
|
name: z.ZodString;
|
|
160
160
|
}, z.core.$strip>>>>;
|
|
161
|
-
grantFile: z.ZodString
|
|
161
|
+
grantFile: z.ZodOptional<z.ZodString>;
|
|
162
162
|
trustedPrincipalKeys: z.ZodArray<z.ZodString>;
|
|
163
163
|
policyFile: z.ZodString;
|
|
164
164
|
facts: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
package/dist/config.js
CHANGED
|
@@ -53,7 +53,8 @@ export const GatewayConfigSchema = z.object({
|
|
|
53
53
|
upstream: UpstreamSchema.optional(),
|
|
54
54
|
/** several upstreams behind one gateway and one grant; each tool name must belong to exactly one of them */
|
|
55
55
|
upstreams: z.array(UpstreamSchema.and(z.object({ name: z.string().min(1) }))).min(1).optional(),
|
|
56
|
-
|
|
56
|
+
/** the one grant a stdio gateway serves; over HTTP each connection presents its own, and this is not needed */
|
|
57
|
+
grantFile: z.string().optional(),
|
|
57
58
|
trustedPrincipalKeys: z.array(z.string()).min(1),
|
|
58
59
|
policyFile: z.string(),
|
|
59
60
|
facts: z.array(FactSchema).default([]),
|
|
@@ -76,7 +77,7 @@ export function loadConfig(path) {
|
|
|
76
77
|
return {
|
|
77
78
|
...cfg,
|
|
78
79
|
identity: { keyFile: r(cfg.identity.keyFile) },
|
|
79
|
-
grantFile: r(cfg.grantFile),
|
|
80
|
+
...(cfg.grantFile ? { grantFile: r(cfg.grantFile) } : {}),
|
|
80
81
|
trustedPrincipalKeys: cfg.trustedPrincipalKeys.map(r),
|
|
81
82
|
policyFile: r(cfg.policyFile),
|
|
82
83
|
receiptsDir: r(cfg.receiptsDir),
|
package/dist/crypto.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare function writeKeyPair(kp: KeyPair, dir: string, name: string): {
|
|
|
22
22
|
export declare function loadPrivateKey(path: string): KeyPair;
|
|
23
23
|
export declare function loadPublicKey(path: string): PublicKeyRef;
|
|
24
24
|
/** A public key from its SPKI PEM text, as found in a .pub file or a conformance vector. */
|
|
25
|
+
/** The SPKI PEM of a public key: what a grant carries as `agentKey`, and what `.pub` files hold. */
|
|
26
|
+
export declare function publicKeyToPem(publicKey: KeyObject): string;
|
|
25
27
|
export declare function publicKeyFromPem(pem: string): PublicKeyRef;
|
|
26
28
|
export interface Envelope {
|
|
27
29
|
payloadType: string;
|
package/dist/crypto.js
CHANGED
|
@@ -53,6 +53,10 @@ export function loadPublicKey(path) {
|
|
|
53
53
|
return publicKeyFromPem(readFileSync(path, "utf8"));
|
|
54
54
|
}
|
|
55
55
|
/** A public key from its SPKI PEM text, as found in a .pub file or a conformance vector. */
|
|
56
|
+
/** The SPKI PEM of a public key: what a grant carries as `agentKey`, and what `.pub` files hold. */
|
|
57
|
+
export function publicKeyToPem(publicKey) {
|
|
58
|
+
return publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
59
|
+
}
|
|
56
60
|
export function publicKeyFromPem(pem) {
|
|
57
61
|
const publicKey = createPublicKey(pem);
|
|
58
62
|
return { publicKey, keyid: keyidOf(publicKey) };
|
package/dist/delegation.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Envelope, type KeyPair, type PublicKeyRef } from "./crypto.ts";
|
|
3
3
|
export declare const DELEGATION_TYPE = "application/vnd.agent-custody.delegation+json";
|
|
4
|
+
/** the longest chain a verifier walks: the principal's grant and up to three delegations below it */
|
|
5
|
+
export declare const MAX_DELEGATION_DEPTH = 4;
|
|
4
6
|
export declare const DelegationSchema: z.ZodObject<{
|
|
5
7
|
version: z.ZodLiteral<"0.1">;
|
|
6
8
|
principal: z.ZodString;
|
|
@@ -8,17 +10,48 @@ export declare const DelegationSchema: z.ZodObject<{
|
|
|
8
10
|
scopes: z.ZodArray<z.ZodString>;
|
|
9
11
|
issuedAt: z.ZodISODateTime;
|
|
10
12
|
expiresAt: z.ZodISODateTime;
|
|
13
|
+
agentKey: z.ZodOptional<z.ZodString>;
|
|
14
|
+
parent: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
payloadType: z.ZodString;
|
|
16
|
+
payload: z.ZodString;
|
|
17
|
+
signatures: z.ZodArray<z.ZodObject<{
|
|
18
|
+
keyid: z.ZodString;
|
|
19
|
+
sig: z.ZodString;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
11
22
|
}, z.core.$strip>;
|
|
12
23
|
export type Delegation = z.infer<typeof DelegationSchema>;
|
|
13
24
|
export declare function createDelegation(principalKey: KeyPair, d: Delegation): Envelope;
|
|
25
|
+
export interface SubDelegation {
|
|
26
|
+
agent: string;
|
|
27
|
+
scopes: string[];
|
|
28
|
+
issuedAt?: string;
|
|
29
|
+
expiresAt?: string;
|
|
30
|
+
/** the sub-agent's own public key, so it may delegate further */
|
|
31
|
+
agentKey?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The parent's payload as written, without verifying it; the verifier does that. */
|
|
34
|
+
export declare function decodeDelegation(env: Envelope): Delegation | null;
|
|
35
|
+
/**
|
|
36
|
+
* An agent delegates part of its grant to a sub-agent. `agentKey` is the delegating agent's key, the one its own grant
|
|
37
|
+
* names. The result embeds the parent; it is refused here, before signing, when it asks for more than the parent has.
|
|
38
|
+
*/
|
|
39
|
+
export declare function delegateFrom(parent: Envelope, agentKey: KeyPair, sub: SubDelegation): Envelope;
|
|
14
40
|
export type DelegationVerifyResult = {
|
|
15
41
|
ok: true;
|
|
42
|
+
/** the grant the receipt was issued under: the leaf of the chain */
|
|
16
43
|
delegation: Delegation;
|
|
44
|
+
/** the principal's key: the signer of the root grant */
|
|
17
45
|
keyid: string;
|
|
46
|
+
/** root first, leaf last; one entry for a direct grant */
|
|
47
|
+
chain: Delegation[];
|
|
18
48
|
} | {
|
|
19
49
|
ok: false;
|
|
20
50
|
error: string;
|
|
21
51
|
};
|
|
22
|
-
|
|
52
|
+
/** Verifies a grant, walking its chain to a grant signed by one of the trusted principal keys. */
|
|
53
|
+
export declare function verifyDelegation(env: Envelope, trustedPrincipals: PublicKeyRef[], depth?: number): DelegationVerifyResult;
|
|
23
54
|
/** True when `at` (ISO) lies inside the grant's validity window. */
|
|
24
55
|
export declare function delegationValidAt(d: Delegation, at: string): boolean;
|
|
56
|
+
/** "principal → agent → sub-agent", for reports. */
|
|
57
|
+
export declare function describeChain(chain: Delegation[]): string;
|
package/dist/delegation.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// A delegation grant: a principal signs a statement that an agent may use certain tools for a window of time.
|
|
2
|
+
//
|
|
3
|
+
// A grant may carry the agent's own public key (`agentKey`); an agent so named may delegate to a sub-agent by signing
|
|
4
|
+
// a narrower grant that embeds the grant it came from (`parent`). A verifier walks the chain to the principal: every
|
|
5
|
+
// link is signed by the key its parent names, every scope is one its parent holds, every window lies inside its
|
|
6
|
+
// parent's, and the principal is the same throughout. The receipt then names the sub-agent as the agent and the
|
|
7
|
+
// principal as the principal, exactly as with a direct grant, and the whole chain travels inside the receipt.
|
|
2
8
|
import { z } from "zod";
|
|
3
|
-
import { dsseSign, dsseVerify } from "./crypto.js";
|
|
9
|
+
import { dsseSign, dsseVerify, publicKeyFromPem } from "./crypto.js";
|
|
4
10
|
export const DELEGATION_TYPE = "application/vnd.agent-custody.delegation+json";
|
|
11
|
+
/** the longest chain a verifier walks: the principal's grant and up to three delegations below it */
|
|
12
|
+
export const MAX_DELEGATION_DEPTH = 4;
|
|
13
|
+
const EnvelopeSchema = z.object({ payloadType: z.string(), payload: z.string(), signatures: z.array(z.object({ keyid: z.string(), sig: z.string() })).min(1) });
|
|
5
14
|
export const DelegationSchema = z.object({
|
|
6
15
|
version: z.literal("0.1"),
|
|
7
16
|
principal: z.string().min(1),
|
|
@@ -9,23 +18,98 @@ export const DelegationSchema = z.object({
|
|
|
9
18
|
scopes: z.array(z.string().min(1)).min(1),
|
|
10
19
|
issuedAt: z.iso.datetime(),
|
|
11
20
|
expiresAt: z.iso.datetime(),
|
|
21
|
+
/** the agent's own public key, SPKI PEM; with it the agent may delegate to a sub-agent */
|
|
22
|
+
agentKey: z.string().min(1).optional(),
|
|
23
|
+
/** the grant this one was delegated from; the chain ends at a grant signed by a trusted principal */
|
|
24
|
+
parent: EnvelopeSchema.optional(),
|
|
12
25
|
});
|
|
13
26
|
export function createDelegation(principalKey, d) {
|
|
14
27
|
return dsseSign(DELEGATION_TYPE, DelegationSchema.parse(d), principalKey);
|
|
15
28
|
}
|
|
16
|
-
|
|
29
|
+
/** The parent's payload as written, without verifying it; the verifier does that. */
|
|
30
|
+
export function decodeDelegation(env) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = DelegationSchema.safeParse(JSON.parse(Buffer.from(env.payload, "base64").toString("utf8")));
|
|
33
|
+
return parsed.success ? parsed.data : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* An agent delegates part of its grant to a sub-agent. `agentKey` is the delegating agent's key, the one its own grant
|
|
41
|
+
* names. The result embeds the parent; it is refused here, before signing, when it asks for more than the parent has.
|
|
42
|
+
*/
|
|
43
|
+
export function delegateFrom(parent, agentKey, sub) {
|
|
44
|
+
const p = decodeDelegation(parent);
|
|
45
|
+
if (!p)
|
|
46
|
+
throw new Error("the parent grant is not a delegation");
|
|
47
|
+
if (!p.agentKey)
|
|
48
|
+
throw new Error(`the parent grant names no agent key, so ${p.agent} cannot delegate`);
|
|
49
|
+
if (publicKeyFromPem(p.agentKey).keyid !== agentKey.keyid)
|
|
50
|
+
throw new Error(`this key is not the one the parent grant names for ${p.agent}`);
|
|
51
|
+
const extra = sub.scopes.filter((s) => !p.scopes.includes(s));
|
|
52
|
+
if (extra.length)
|
|
53
|
+
throw new Error(`a sub-agent cannot be given scopes its delegator lacks: ${extra.join(", ")}`);
|
|
54
|
+
const issuedAt = sub.issuedAt ?? new Date().toISOString();
|
|
55
|
+
const expiresAt = sub.expiresAt ?? p.expiresAt;
|
|
56
|
+
if (Date.parse(issuedAt) < Date.parse(p.issuedAt) || Date.parse(expiresAt) > Date.parse(p.expiresAt))
|
|
57
|
+
throw new Error("a sub-agent's window must lie inside its delegator's");
|
|
58
|
+
return createDelegation(agentKey, { version: "0.1", principal: p.principal, agent: sub.agent, scopes: sub.scopes, issuedAt, expiresAt, ...(sub.agentKey ? { agentKey: sub.agentKey } : {}), parent });
|
|
59
|
+
}
|
|
60
|
+
/** Verifies a grant, walking its chain to a grant signed by one of the trusted principal keys. */
|
|
61
|
+
export function verifyDelegation(env, trustedPrincipals, depth = 1) {
|
|
17
62
|
if (env.payloadType !== DELEGATION_TYPE)
|
|
18
63
|
return { ok: false, error: `unexpected payloadType ${env.payloadType}` };
|
|
19
|
-
|
|
64
|
+
if (depth > MAX_DELEGATION_DEPTH)
|
|
65
|
+
return { ok: false, error: `delegation chain deeper than ${MAX_DELEGATION_DEPTH}` };
|
|
66
|
+
const unverified = decodeDelegation(env);
|
|
67
|
+
if (!unverified)
|
|
68
|
+
return { ok: false, error: "invalid delegation" };
|
|
69
|
+
if (!unverified.parent) {
|
|
70
|
+
const r = dsseVerify(env, trustedPrincipals);
|
|
71
|
+
if (!r.ok)
|
|
72
|
+
return r;
|
|
73
|
+
const parsed = DelegationSchema.safeParse(r.payload);
|
|
74
|
+
if (!parsed.success)
|
|
75
|
+
return { ok: false, error: `invalid delegation: ${parsed.error.message}` };
|
|
76
|
+
return { ok: true, delegation: parsed.data, keyid: r.keyid, chain: [parsed.data] };
|
|
77
|
+
}
|
|
78
|
+
const up = verifyDelegation(unverified.parent, trustedPrincipals, depth + 1);
|
|
79
|
+
if (!up.ok)
|
|
80
|
+
return { ok: false, error: `link ${depth}: ${up.error}` };
|
|
81
|
+
const parent = up.delegation;
|
|
82
|
+
if (!parent.agentKey)
|
|
83
|
+
return { ok: false, error: `link ${depth}: ${parent.agent} holds no agent key and cannot delegate` };
|
|
84
|
+
let parentKey;
|
|
85
|
+
try {
|
|
86
|
+
parentKey = publicKeyFromPem(parent.agentKey);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { ok: false, error: `link ${depth}: the agent key named for ${parent.agent} is not a public key` };
|
|
90
|
+
}
|
|
91
|
+
const r = dsseVerify(env, [parentKey]);
|
|
20
92
|
if (!r.ok)
|
|
21
|
-
return r;
|
|
93
|
+
return { ok: false, error: `link ${depth}: not signed by ${parent.agent}'s key: ${r.error}` };
|
|
22
94
|
const parsed = DelegationSchema.safeParse(r.payload);
|
|
23
95
|
if (!parsed.success)
|
|
24
96
|
return { ok: false, error: `invalid delegation: ${parsed.error.message}` };
|
|
25
|
-
|
|
97
|
+
const d = parsed.data;
|
|
98
|
+
if (d.principal !== parent.principal)
|
|
99
|
+
return { ok: false, error: `link ${depth}: principal changed from ${parent.principal} to ${d.principal}` };
|
|
100
|
+
const extra = d.scopes.filter((s) => !parent.scopes.includes(s));
|
|
101
|
+
if (extra.length)
|
|
102
|
+
return { ok: false, error: `link ${depth}: ${d.agent} was given scopes ${parent.agent} does not hold: ${extra.join(", ")}` };
|
|
103
|
+
if (Date.parse(d.issuedAt) < Date.parse(parent.issuedAt) || Date.parse(d.expiresAt) > Date.parse(parent.expiresAt))
|
|
104
|
+
return { ok: false, error: `link ${depth}: ${d.agent}'s window is not inside ${parent.agent}'s` };
|
|
105
|
+
return { ok: true, delegation: d, keyid: up.keyid, chain: [...up.chain, d] };
|
|
26
106
|
}
|
|
27
107
|
/** True when `at` (ISO) lies inside the grant's validity window. */
|
|
28
108
|
export function delegationValidAt(d, at) {
|
|
29
109
|
const t = Date.parse(at);
|
|
30
110
|
return t >= Date.parse(d.issuedAt) && t <= Date.parse(d.expiresAt);
|
|
31
111
|
}
|
|
112
|
+
/** "principal → agent → sub-agent", for reports. */
|
|
113
|
+
export function describeChain(chain) {
|
|
114
|
+
return [chain[0].principal, ...chain.map((d) => d.agent)].join(" → ");
|
|
115
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type IncomingMessage } from "node:http";
|
|
2
|
+
import type { Envelope } from "./crypto.ts";
|
|
3
|
+
import { type GatewayHost } from "./gateway.ts";
|
|
4
|
+
export declare const GRANT_HEADER = "x-agent-custody-grant";
|
|
5
|
+
/** The header value that presents a grant: the DSSE envelope as base64url JSON. Send it as `Authorization: Bearer <value>` or as `X-Agent-Custody-Grant`. */
|
|
6
|
+
export declare function grantHeader(envelope: Envelope): string;
|
|
7
|
+
export declare function parseGrantHeader(req: IncomingMessage): Envelope | null;
|
|
8
|
+
export interface HttpGatewayOptions {
|
|
9
|
+
port: number;
|
|
10
|
+
host?: string;
|
|
11
|
+
/** the MCP endpoint path; default /mcp */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** a session with no request for this long is closed; default thirty minutes */
|
|
14
|
+
idleMs?: number;
|
|
15
|
+
/** where session events are reported; default stderr */
|
|
16
|
+
log?: (message: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface RunningHttpGateway {
|
|
19
|
+
url: string;
|
|
20
|
+
/** live sessions by MCP session id */
|
|
21
|
+
sessions(): {
|
|
22
|
+
id: string;
|
|
23
|
+
agent: string;
|
|
24
|
+
principal: string;
|
|
25
|
+
since: string;
|
|
26
|
+
}[];
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** Serves a gateway host as an MCP server over Streamable HTTP, a session per grant. */
|
|
30
|
+
export declare function serveHttp(host: GatewayHost, opts: HttpGatewayOptions): Promise<RunningHttpGateway>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// The gateway over HTTP: one process, many agents, each connection under its own grant. A platform team runs one
|
|
2
|
+
// gateway in front of the tools; every agent connects with MCP over Streamable HTTP and presents the grant its
|
|
3
|
+
// principal signed, and the gateway opens a session for exactly that grant. Sessions share the upstreams, the policy,
|
|
4
|
+
// the key, and the log; each has its own consumed facts and its own receipts, and none can see another's tools.
|
|
5
|
+
// The grant is the credential: it is signed by a principal key the gateway trusts, so nothing else is needed to
|
|
6
|
+
// authenticate a connection, and a grant that is expired, revoked by time, or signed by a stranger gets 403 with the
|
|
7
|
+
// reason in the body. (Not 401: the MCP client transport treats 401 as an OAuth challenge and hides the body.)
|
|
8
|
+
// Bind to loopback or put TLS in front; the transport itself is plain HTTP.
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
+
import { CallToolRequestSchema, isInitializeRequest, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { GATEWAY_VERSION } from "./gateway.js";
|
|
15
|
+
export const GRANT_HEADER = "x-agent-custody-grant";
|
|
16
|
+
/** The header value that presents a grant: the DSSE envelope as base64url JSON. Send it as `Authorization: Bearer <value>` or as `X-Agent-Custody-Grant`. */
|
|
17
|
+
export function grantHeader(envelope) {
|
|
18
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64url");
|
|
19
|
+
}
|
|
20
|
+
export function parseGrantHeader(req) {
|
|
21
|
+
const explicit = req.headers[GRANT_HEADER];
|
|
22
|
+
const auth = req.headers.authorization ?? "";
|
|
23
|
+
const raw = typeof explicit === "string" && explicit ? explicit : auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
24
|
+
if (!raw)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
28
|
+
return parsed && typeof parsed === "object" && typeof parsed.payload === "string" && Array.isArray(parsed.signatures) ? parsed : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Serves a gateway host as an MCP server over Streamable HTTP, a session per grant. */
|
|
35
|
+
export async function serveHttp(host, opts) {
|
|
36
|
+
const bind = opts.host ?? "127.0.0.1";
|
|
37
|
+
const path = opts.path ?? "/mcp";
|
|
38
|
+
const idleMs = opts.idleMs ?? 30 * 60_000;
|
|
39
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
40
|
+
const sessions = new Map();
|
|
41
|
+
const json = (res, status, body) => {
|
|
42
|
+
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
43
|
+
res.end(JSON.stringify(body));
|
|
44
|
+
};
|
|
45
|
+
const rpcError = (res, status, message) => json(res, status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null });
|
|
46
|
+
const readBody = async (req) => {
|
|
47
|
+
let text = "";
|
|
48
|
+
for await (const chunk of req) {
|
|
49
|
+
text += chunk;
|
|
50
|
+
if (text.length > 4_194_304)
|
|
51
|
+
throw new Error("body larger than 4 MB");
|
|
52
|
+
}
|
|
53
|
+
return text ? JSON.parse(text) : undefined;
|
|
54
|
+
};
|
|
55
|
+
const closeSession = async (id, why) => {
|
|
56
|
+
const s = sessions.get(id);
|
|
57
|
+
if (!s)
|
|
58
|
+
return;
|
|
59
|
+
sessions.delete(id);
|
|
60
|
+
log(`agent-custody gateway: session ${id.slice(0, 8)} for ${s.gateway.agentId} closed (${why})`);
|
|
61
|
+
await s.gateway.close();
|
|
62
|
+
await s.transport.close().catch(() => { });
|
|
63
|
+
};
|
|
64
|
+
const openSession = async (req, res, body) => {
|
|
65
|
+
const envelope = parseGrantHeader(req);
|
|
66
|
+
if (!envelope)
|
|
67
|
+
return rpcError(res, 403, `a grant is required: send the delegation envelope as base64url in Authorization: Bearer or ${GRANT_HEADER}`);
|
|
68
|
+
let gateway;
|
|
69
|
+
try {
|
|
70
|
+
gateway = host.open(envelope);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return rpcError(res, 403, e instanceof Error ? e.message : String(e));
|
|
74
|
+
}
|
|
75
|
+
const transport = new StreamableHTTPServerTransport({
|
|
76
|
+
sessionIdGenerator: () => randomUUID(),
|
|
77
|
+
onsessioninitialized: (id) => {
|
|
78
|
+
sessions.set(id, { gateway, transport, server, since: new Date().toISOString(), lastSeen: Date.now() });
|
|
79
|
+
log(`agent-custody gateway: session ${id.slice(0, 8)} opened for agent=${gateway.agentId} principal=${gateway.delegation.principal} scopes=[${gateway.delegation.scopes.join(", ")}]`);
|
|
80
|
+
},
|
|
81
|
+
onsessionclosed: (id) => void closeSession(id, "closed by the client"),
|
|
82
|
+
});
|
|
83
|
+
const server = new Server({ name: "agent-custody-gateway", version: GATEWAY_VERSION }, { capabilities: { tools: {} } });
|
|
84
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await gateway.listTools() }));
|
|
85
|
+
server.setRequestHandler(CallToolRequestSchema, async (r) => gateway.handleCall(r.params));
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
await transport.handleRequest(req, res, body);
|
|
88
|
+
};
|
|
89
|
+
const handler = async (req, res) => {
|
|
90
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
91
|
+
if (req.method === "GET" && url.pathname === "/health")
|
|
92
|
+
return json(res, 200, { ok: true, sessions: sessions.size, keyid: host.keyid });
|
|
93
|
+
if (url.pathname !== path)
|
|
94
|
+
return json(res, 404, { error: "not found" });
|
|
95
|
+
try {
|
|
96
|
+
const sid = req.headers["mcp-session-id"];
|
|
97
|
+
const existing = typeof sid === "string" ? sessions.get(sid) : undefined;
|
|
98
|
+
if (existing) {
|
|
99
|
+
existing.lastSeen = Date.now();
|
|
100
|
+
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
101
|
+
await existing.transport.handleRequest(req, res, body);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (typeof sid === "string")
|
|
105
|
+
return rpcError(res, 404, "unknown or expired session; initialize again with your grant");
|
|
106
|
+
if (req.method !== "POST")
|
|
107
|
+
return rpcError(res, 400, "initialize first: POST an initialize request with your grant");
|
|
108
|
+
const body = await readBody(req);
|
|
109
|
+
if (!isInitializeRequest(body))
|
|
110
|
+
return rpcError(res, 400, "the first request of a session must be initialize");
|
|
111
|
+
await openSession(req, res, body);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
if (!res.headersSent)
|
|
115
|
+
rpcError(res, 500, e instanceof Error ? e.message : String(e));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const server = createServer((req, res) => void handler(req, res));
|
|
119
|
+
const reaper = setInterval(() => {
|
|
120
|
+
const cutoff = Date.now() - idleMs;
|
|
121
|
+
for (const [id, s] of sessions)
|
|
122
|
+
if (s.lastSeen < cutoff)
|
|
123
|
+
void closeSession(id, "idle");
|
|
124
|
+
}, Math.min(idleMs, 60_000));
|
|
125
|
+
reaper.unref?.();
|
|
126
|
+
await new Promise((resolve) => server.listen(opts.port, bind, resolve));
|
|
127
|
+
const { port } = server.address();
|
|
128
|
+
return {
|
|
129
|
+
url: `http://${bind}:${port}${path}`,
|
|
130
|
+
sessions: () => [...sessions.entries()].map(([id, s]) => ({ id, agent: s.gateway.agentId, principal: s.gateway.delegation.principal, since: s.since })),
|
|
131
|
+
async close() {
|
|
132
|
+
clearInterval(reaper);
|
|
133
|
+
for (const id of [...sessions.keys()])
|
|
134
|
+
await closeSession(id, "server closing");
|
|
135
|
+
server.closeAllConnections?.();
|
|
136
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CallToolResult, type Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { GatewayConfig } from "./config.ts";
|
|
3
|
+
import { type Envelope } from "./crypto.ts";
|
|
3
4
|
import { type Delegation } from "./delegation.ts";
|
|
4
5
|
import { type LogSink } from "./log-sink.ts";
|
|
5
6
|
import { type ReceiptExporter } from "./otel.ts";
|
|
@@ -31,6 +32,19 @@ export interface GatewayOptions {
|
|
|
31
32
|
/** told about every receipt after it is written, in place of the exporter the config names */
|
|
32
33
|
exporter?: ReceiptExporter;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The shared part of a gateway: the key, the policy, the issuer, the log, the upstreams, and the fact lookups. One host
|
|
37
|
+
* serves many sessions, each opened with its own grant; over stdio there is exactly one, over HTTP one per connection.
|
|
38
|
+
*/
|
|
39
|
+
export interface GatewayHost {
|
|
40
|
+
keyid: string;
|
|
41
|
+
/** a session for this grant: the grant is verified against the trusted principal keys and its validity window first */
|
|
42
|
+
open(grantEnvelope: Envelope): Gateway;
|
|
43
|
+
/** closes the upstreams; every session opened from this host is finished with */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createGatewayHost(cfg: GatewayConfig, options?: GatewayOptions): Promise<GatewayHost>;
|
|
47
|
+
/** One gateway for the grant the config names: what `agent-custody gateway` serves over stdio. Closing it closes the host. */
|
|
34
48
|
export declare function createGateway(cfg: GatewayConfig, options?: GatewayOptions): Promise<Gateway>;
|
|
35
49
|
/** Exposes the gateway as an MCP server over stdio. Everything diagnostic must go to stderr. */
|
|
36
50
|
export declare function serveStdio(gw: Gateway): Promise<void>;
|