@adtl/gateway 0.5.1 → 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 +97 -8
- package/dist/approvals.d.ts +22 -6
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +56 -26
- package/dist/approvals.js.map +1 -1
- package/dist/audit.d.ts +8 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/auditCli.d.ts +2 -0
- package/dist/auditCli.d.ts.map +1 -1
- package/dist/auditCli.js +13 -10
- package/dist/auditCli.js.map +1 -1
- package/dist/cli.js +17 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +285 -21
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +49 -1
- package/dist/config.js.map +1 -1
- package/dist/elicitation.d.ts +25 -0
- package/dist/elicitation.d.ts.map +1 -0
- package/dist/elicitation.js +46 -0
- package/dist/elicitation.js.map +1 -0
- package/dist/gateway.d.ts +26 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +85 -8
- package/dist/gateway.js.map +1 -1
- package/dist/hooks/claudeCode.d.ts +4 -0
- package/dist/hooks/claudeCode.d.ts.map +1 -1
- package/dist/hooks/claudeCode.js +8 -1
- package/dist/hooks/claudeCode.js.map +1 -1
- package/dist/hooks/run.d.ts.map +1 -1
- package/dist/hooks/run.js +6 -3
- package/dist/hooks/run.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/init/presets.d.ts.map +1 -1
- package/dist/init/presets.js +54 -0
- package/dist/init/presets.js.map +1 -1
- package/dist/invoker.d.ts +21 -1
- package/dist/invoker.d.ts.map +1 -1
- package/dist/invoker.js +19 -2
- package/dist/invoker.js.map +1 -1
- package/dist/ledgerCli.d.ts +1 -1
- package/dist/ledgerCli.d.ts.map +1 -1
- package/dist/ledgerCli.js +9 -3
- package/dist/ledgerCli.js.map +1 -1
- package/dist/ntfy.d.ts +3 -1
- package/dist/ntfy.d.ts.map +1 -1
- package/dist/ntfy.js +2 -1
- package/dist/ntfy.js.map +1 -1
- package/dist/playgroundCli.d.ts +8 -0
- package/dist/playgroundCli.d.ts.map +1 -0
- package/dist/playgroundCli.js +57 -0
- package/dist/playgroundCli.js.map +1 -0
- package/dist/receipt.d.ts +62 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +117 -0
- package/dist/receipt.js.map +1 -0
- package/dist/receiptCli.d.ts +8 -0
- package/dist/receiptCli.d.ts.map +1 -0
- package/dist/receiptCli.js +87 -0
- package/dist/receiptCli.js.map +1 -0
- package/dist/roles.d.ts +9 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +41 -0
- package/dist/roles.js.map +1 -0
- package/dist/startup.d.ts +9 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +69 -7
- package/dist/startup.js.map +1 -1
- package/package.json +5 -4
- package/playground/fake-github.mjs +98 -0
- package/playground/fake-memory.mjs +130 -0
- package/playground/index.html +411 -0
- package/playground/playground.yaml +40 -0
- package/playground/server.mjs +290 -0
- package/src/approvals.ts +63 -26
- package/src/audit.ts +5 -0
- package/src/auditCli.ts +11 -8
- package/src/cli.ts +16 -1
- package/src/config.ts +54 -1
- package/src/elicitation.ts +69 -0
- package/src/gateway.ts +96 -9
- package/src/hooks/claudeCode.ts +13 -2
- package/src/hooks/run.ts +6 -3
- package/src/index.ts +5 -0
- package/src/init/presets.ts +54 -0
- package/src/invoker.ts +39 -2
- package/src/ledgerCli.ts +9 -3
- package/src/ntfy.ts +4 -1
- package/src/playgroundCli.ts +60 -0
- package/src/receipt.ts +148 -0
- package/src/receiptCli.ts +85 -0
- package/src/roles.ts +55 -0
- package/src/startup.ts +68 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `adtl-gateway playground [--port 4317] [--no-open]`: the web playground, with
|
|
3
|
+
* the real gateway in front of fake GitHub and memory servers. Listens on
|
|
4
|
+
* 127.0.0.1 only and opens the browser unless --no-open is given.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
export const PLAYGROUND_USAGE = `Usage: adtl-gateway playground [--port <n>] [--no-open]
|
|
8
|
+
|
|
9
|
+
Runs the ADTL gateway in front of fake GitHub and memory servers, with a
|
|
10
|
+
browser UI: three test cases, every call's label and decision, the session
|
|
11
|
+
label, approvals, the config and the audit log. Nothing leaves your machine.
|
|
12
|
+
|
|
13
|
+
--port <n> Port on 127.0.0.1 (default 4317; 0 picks a free one).
|
|
14
|
+
--no-open Don't open the browser.`;
|
|
15
|
+
export function parsePlaygroundArgs(argv) {
|
|
16
|
+
let port = 4317;
|
|
17
|
+
let open = true;
|
|
18
|
+
let help = false;
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a === "--port")
|
|
22
|
+
port = Number(argv[++i]);
|
|
23
|
+
else if (a === "--no-open")
|
|
24
|
+
open = false;
|
|
25
|
+
else if (a === "-h" || a === "--help")
|
|
26
|
+
help = true;
|
|
27
|
+
else
|
|
28
|
+
throw new Error(`Unknown argument: ${a}\n\n${PLAYGROUND_USAGE}`);
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
31
|
+
throw new Error(`--port must be a number from 0 to 65535\n\n${PLAYGROUND_USAGE}`);
|
|
32
|
+
return { port, open, help };
|
|
33
|
+
}
|
|
34
|
+
function openBrowser(url) {
|
|
35
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
36
|
+
try {
|
|
37
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => { }).unref();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No browser to open: the URL is printed anyway.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runPlayground(argv) {
|
|
44
|
+
const opts = parsePlaygroundArgs(argv);
|
|
45
|
+
if (opts.help)
|
|
46
|
+
return void console.log(PLAYGROUND_USAGE);
|
|
47
|
+
// Plain JavaScript shipped next to dist/, not compiled.
|
|
48
|
+
const mod = (await import(new URL("../playground/server.mjs", import.meta.url).href));
|
|
49
|
+
const pg = await mod.startPlayground({ port: opts.port });
|
|
50
|
+
console.log(`ADTL playground: ${pg.url} (Ctrl-C to stop)`);
|
|
51
|
+
if (opts.open)
|
|
52
|
+
openBrowser(pg.url);
|
|
53
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
54
|
+
process.once(sig, () => void pg.close().finally(() => process.exit(0)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=playgroundCli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playgroundCli.js","sourceRoot":"","sources":["../src/playgroundCli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Signer } from "@adtl/labels";
|
|
2
|
+
import { type AuditEntry } from "./audit.js";
|
|
3
|
+
export declare const RECEIPT_TYPE = "adtl.receipt.v1";
|
|
4
|
+
export interface ReceiptCall {
|
|
5
|
+
ts: string;
|
|
6
|
+
event: AuditEntry["event"];
|
|
7
|
+
server: string;
|
|
8
|
+
tool?: string;
|
|
9
|
+
resource?: string;
|
|
10
|
+
prompt?: string;
|
|
11
|
+
outcome: AuditEntry["outcome"];
|
|
12
|
+
decision?: string;
|
|
13
|
+
approver?: string;
|
|
14
|
+
callHash?: string;
|
|
15
|
+
/** Integrity and confidentiality of the result. */
|
|
16
|
+
label?: {
|
|
17
|
+
integrity: string;
|
|
18
|
+
confidentiality: string;
|
|
19
|
+
};
|
|
20
|
+
/** Hash of this call's audit log line. */
|
|
21
|
+
line: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Receipt {
|
|
24
|
+
type: typeof RECEIPT_TYPE;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
counts: {
|
|
28
|
+
calls: number;
|
|
29
|
+
forwarded: number;
|
|
30
|
+
blocked: number;
|
|
31
|
+
asked: number;
|
|
32
|
+
upstreamErrors: number;
|
|
33
|
+
approved: number;
|
|
34
|
+
denied: number;
|
|
35
|
+
};
|
|
36
|
+
calls: ReceiptCall[];
|
|
37
|
+
/** Public key that signed it, `ed25519:…`. */
|
|
38
|
+
signer: string;
|
|
39
|
+
signature: string;
|
|
40
|
+
}
|
|
41
|
+
/** Sessions in the audit log, with their number of calls and last activity. */
|
|
42
|
+
export declare function listSessions(logPath: string): {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
calls: number;
|
|
45
|
+
last: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** Build and sign the receipt for `sessionId` from a verified audit log. */
|
|
48
|
+
export declare function buildReceipt(logPath: string, sessionId: string, signer: Signer, now?: Date): Receipt;
|
|
49
|
+
export interface ReceiptCheck {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
problem?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check a receipt's signature, that it is signed by `trustedKey` if given,
|
|
55
|
+
* and, with `logPath`, that the log is intact and the receipt lists exactly
|
|
56
|
+
* the session's lines from its first call to its last.
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyReceipt(receipt: Receipt, opts?: {
|
|
59
|
+
logPath?: string;
|
|
60
|
+
trustedKey?: string;
|
|
61
|
+
}): ReceiptCheck;
|
|
62
|
+
//# sourceMappingURL=receipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAeA,OAAO,EAAkC,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7D,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,KAAK,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,YAAY,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACvI,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAcD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CASlG;AAsBD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAqBhH;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CAqBlH"}
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session receipts: a signed summary of every call a session made through
|
|
3
|
+
* the gateway and the Claude Code hooks, built from the hash-chained audit log.
|
|
4
|
+
*
|
|
5
|
+
* Agents can misreport what they did; the audit log is written outside the
|
|
6
|
+
* model. A receipt names the session, lists each call (tool, outcome,
|
|
7
|
+
* decision, approver, call hash, result label, and the hash of its audit log
|
|
8
|
+
* line) with counts, and is signed with the machine's Ed25519 key. Anyone with
|
|
9
|
+
* the public key can check it, and with the log at hand, that it lists every
|
|
10
|
+
* line the session wrote between its first and last call.
|
|
11
|
+
*
|
|
12
|
+
* Receipts cover what crossed the gateway and the hooks, not everything the
|
|
13
|
+
* agent did. Arguments and results are never included.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { verifyCanonical, signCanonical } from "@adtl/labels";
|
|
17
|
+
import { verifyAuditLog } from "./audit.js";
|
|
18
|
+
export const RECEIPT_TYPE = "adtl.receipt.v1";
|
|
19
|
+
function chainedEntries(logPath) {
|
|
20
|
+
const v = verifyAuditLog(logPath);
|
|
21
|
+
if (!v.ok)
|
|
22
|
+
throw new Error(`${logPath}: the audit log was tampered with (${v.problem}); refusing to issue a receipt`);
|
|
23
|
+
return readFileSync(logPath, "utf8")
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter((l) => l.trim())
|
|
26
|
+
.map((l) => JSON.parse(l))
|
|
27
|
+
.filter((e) => typeof e.hash === "string" && typeof e.sessionId === "string");
|
|
28
|
+
}
|
|
29
|
+
/** Sessions in the audit log, with their number of calls and last activity. */
|
|
30
|
+
export function listSessions(logPath) {
|
|
31
|
+
const out = new Map();
|
|
32
|
+
for (const e of chainedEntries(logPath)) {
|
|
33
|
+
const s = out.get(e.sessionId) ?? { sessionId: e.sessionId, calls: 0, last: e.ts };
|
|
34
|
+
s.calls++;
|
|
35
|
+
if (e.ts > s.last)
|
|
36
|
+
s.last = e.ts;
|
|
37
|
+
out.set(e.sessionId, s);
|
|
38
|
+
}
|
|
39
|
+
return [...out.values()].sort((a, b) => b.last.localeCompare(a.last));
|
|
40
|
+
}
|
|
41
|
+
function receiptCall(e) {
|
|
42
|
+
const label = e.label;
|
|
43
|
+
return {
|
|
44
|
+
ts: e.ts,
|
|
45
|
+
event: e.event,
|
|
46
|
+
server: e.server,
|
|
47
|
+
...(e.tool !== undefined ? { tool: e.tool } : {}),
|
|
48
|
+
...(e.resource !== undefined ? { resource: e.resource } : {}),
|
|
49
|
+
...(e.prompt !== undefined ? { prompt: e.prompt } : {}),
|
|
50
|
+
outcome: e.outcome,
|
|
51
|
+
...(e.decision !== undefined ? { decision: e.decision } : {}),
|
|
52
|
+
...(e.approval?.approver !== undefined ? { approver: e.approval.approver } : {}),
|
|
53
|
+
...(typeof e.callHash === "string" ? { callHash: e.callHash } : {}),
|
|
54
|
+
...(typeof label?.integrity === "string" && typeof label.confidentiality === "string"
|
|
55
|
+
? { label: { integrity: label.integrity, confidentiality: label.confidentiality } }
|
|
56
|
+
: {}),
|
|
57
|
+
line: e.hash,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Build and sign the receipt for `sessionId` from a verified audit log. */
|
|
61
|
+
export function buildReceipt(logPath, sessionId, signer, now = new Date()) {
|
|
62
|
+
const entries = chainedEntries(logPath).filter((e) => e.sessionId === sessionId);
|
|
63
|
+
if (entries.length === 0)
|
|
64
|
+
throw new Error(`${logPath} has no entries for session ${sessionId}`);
|
|
65
|
+
const count = (f) => entries.filter(f).length;
|
|
66
|
+
const unsigned = {
|
|
67
|
+
type: RECEIPT_TYPE,
|
|
68
|
+
sessionId,
|
|
69
|
+
createdAt: now.toISOString(),
|
|
70
|
+
counts: {
|
|
71
|
+
calls: entries.length,
|
|
72
|
+
forwarded: count((e) => e.outcome === "forwarded"),
|
|
73
|
+
blocked: count((e) => e.outcome === "blocked"),
|
|
74
|
+
asked: count((e) => e.outcome === "asked"),
|
|
75
|
+
upstreamErrors: count((e) => e.outcome === "upstream_error"),
|
|
76
|
+
approved: count((e) => e.approval?.outcome === "approved"),
|
|
77
|
+
denied: count((e) => e.approval?.outcome === "denied"),
|
|
78
|
+
},
|
|
79
|
+
calls: entries.map(receiptCall),
|
|
80
|
+
signer: signer.publicKey,
|
|
81
|
+
};
|
|
82
|
+
return { ...unsigned, signature: signCanonical(signer, unsigned) };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check a receipt's signature, that it is signed by `trustedKey` if given,
|
|
86
|
+
* and, with `logPath`, that the log is intact and the receipt lists exactly
|
|
87
|
+
* the session's lines from its first call to its last.
|
|
88
|
+
*/
|
|
89
|
+
export function verifyReceipt(receipt, opts = {}) {
|
|
90
|
+
const { signature, ...unsigned } = receipt;
|
|
91
|
+
if (receipt.type !== RECEIPT_TYPE)
|
|
92
|
+
return { ok: false, problem: `not an ADTL receipt (type ${String(receipt.type)})` };
|
|
93
|
+
if (!verifyCanonical(receipt.signer, unsigned, signature))
|
|
94
|
+
return { ok: false, problem: "the signature doesn't match: the receipt was changed or forged" };
|
|
95
|
+
if (opts.trustedKey && receipt.signer !== opts.trustedKey)
|
|
96
|
+
return { ok: false, problem: `signed by ${receipt.signer}, not the expected key` };
|
|
97
|
+
if (!opts.logPath)
|
|
98
|
+
return { ok: true };
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = chainedEntries(opts.logPath);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return { ok: false, problem: err.message };
|
|
105
|
+
}
|
|
106
|
+
const lines = receipt.calls.map((c) => c.line);
|
|
107
|
+
const first = entries.findIndex((e) => e.hash === lines[0]);
|
|
108
|
+
const last = entries.findIndex((e) => e.hash === lines.at(-1));
|
|
109
|
+
if (first < 0 || last < 0)
|
|
110
|
+
return { ok: false, problem: "the log doesn't contain the receipt's calls" };
|
|
111
|
+
const logged = entries.slice(first, last + 1).filter((e) => e.sessionId === receipt.sessionId);
|
|
112
|
+
if (logged.length !== lines.length || logged.some((e, i) => e.hash !== lines[i])) {
|
|
113
|
+
return { ok: false, problem: "the receipt doesn't list exactly the session's calls in the log" };
|
|
114
|
+
}
|
|
115
|
+
return { ok: true };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=receipt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const RECEIPT_USAGE = "Usage: adtl-gateway receipt list [<log>] [-c <adtl.yaml>]\n adtl-gateway receipt <session> [<log>] [-c <adtl.yaml>] [-o <receipt.json>]\n adtl-gateway receipt verify <receipt.json> [--log <log>] [--key <ed25519:...>]\n adtl-gateway key\n\nA receipt is a signed summary of every call one session made through the\ngateway and the Claude Code hooks: tool, outcome, decision, approver, call\nhash, result label and the hash of each audit log line. It is built from the\nhash-chained audit log (gateway.audit) and signed with this machine's key\n(ADTL_SIGNING_KEY, or ~/.adtl/signing-key.pem).\n\n list Sessions in the log, newest first.\n verify Check the signature; with --log, also that the receipt lists every\n line the session wrote; with --key, who signed it (exit 1 if not).\n key Print this machine's public key, to give to whoever checks receipts.";
|
|
2
|
+
export interface ReceiptCommandResult {
|
|
3
|
+
stdout: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function runKeyCommand(env?: NodeJS.ProcessEnv): Promise<ReceiptCommandResult>;
|
|
7
|
+
export declare function runReceiptCommand(argv: string[], env?: NodeJS.ProcessEnv): Promise<ReceiptCommandResult>;
|
|
8
|
+
//# sourceMappingURL=receiptCli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receiptCli.d.ts","sourceRoot":"","sources":["../src/receiptCli.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,aAAa,o4BAcsD,CAAC;AAEjF,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAIvG;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA4C3H"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `adtl-gateway receipt` and `adtl-gateway key`.
|
|
3
|
+
*
|
|
4
|
+
* adtl-gateway receipt list [<log>] [-c adtl.yaml]
|
|
5
|
+
* adtl-gateway receipt <session> [<log>] [-c adtl.yaml] [-o receipt.json]
|
|
6
|
+
* adtl-gateway receipt verify <receipt.json> [--log <log>] [--key ed25519:…]
|
|
7
|
+
* adtl-gateway key
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { defaultSigningKeyPath, loadOrCreateSigner } from "@adtl/labels";
|
|
11
|
+
import { resolveAuditPath } from "./auditCli.js";
|
|
12
|
+
import { buildReceipt, listSessions, verifyReceipt } from "./receipt.js";
|
|
13
|
+
export const RECEIPT_USAGE = `Usage: adtl-gateway receipt list [<log>] [-c <adtl.yaml>]
|
|
14
|
+
adtl-gateway receipt <session> [<log>] [-c <adtl.yaml>] [-o <receipt.json>]
|
|
15
|
+
adtl-gateway receipt verify <receipt.json> [--log <log>] [--key <ed25519:...>]
|
|
16
|
+
adtl-gateway key
|
|
17
|
+
|
|
18
|
+
A receipt is a signed summary of every call one session made through the
|
|
19
|
+
gateway and the Claude Code hooks: tool, outcome, decision, approver, call
|
|
20
|
+
hash, result label and the hash of each audit log line. It is built from the
|
|
21
|
+
hash-chained audit log (gateway.audit) and signed with this machine's key
|
|
22
|
+
(ADTL_SIGNING_KEY, or ~/.adtl/signing-key.pem).
|
|
23
|
+
|
|
24
|
+
list Sessions in the log, newest first.
|
|
25
|
+
verify Check the signature; with --log, also that the receipt lists every
|
|
26
|
+
line the session wrote; with --key, who signed it (exit 1 if not).
|
|
27
|
+
key Print this machine's public key, to give to whoever checks receipts.`;
|
|
28
|
+
export async function runKeyCommand(env = process.env) {
|
|
29
|
+
const path = defaultSigningKeyPath(env);
|
|
30
|
+
const signer = loadOrCreateSigner(path);
|
|
31
|
+
return { stdout: `${signer.publicKey}\nkey id: ${signer.keyId}\nfile: ${path}`, exitCode: 0 };
|
|
32
|
+
}
|
|
33
|
+
export async function runReceiptCommand(argv, env = process.env) {
|
|
34
|
+
const [cmd, ...rest] = argv;
|
|
35
|
+
if (cmd === undefined || cmd === "-h" || cmd === "--help")
|
|
36
|
+
return { stdout: RECEIPT_USAGE, exitCode: 0 };
|
|
37
|
+
let config;
|
|
38
|
+
let out;
|
|
39
|
+
let log;
|
|
40
|
+
let key;
|
|
41
|
+
const args = [];
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
const a = rest[i];
|
|
44
|
+
if (a === "-c" || a === "--config")
|
|
45
|
+
config = rest[++i];
|
|
46
|
+
else if (a === "-o" || a === "--out")
|
|
47
|
+
out = rest[++i];
|
|
48
|
+
else if (a === "--log")
|
|
49
|
+
log = rest[++i];
|
|
50
|
+
else if (a === "--key")
|
|
51
|
+
key = rest[++i];
|
|
52
|
+
else if (a.startsWith("-"))
|
|
53
|
+
throw new Error(`Unknown argument: ${a}\n\n${RECEIPT_USAGE}`);
|
|
54
|
+
else
|
|
55
|
+
args.push(a);
|
|
56
|
+
}
|
|
57
|
+
if (cmd === "verify") {
|
|
58
|
+
if (args.length !== 1)
|
|
59
|
+
throw new Error(`receipt verify needs one receipt file\n\n${RECEIPT_USAGE}`);
|
|
60
|
+
const receipt = JSON.parse(await readFile(args[0], "utf8"));
|
|
61
|
+
const r = verifyReceipt(receipt, { ...(log ? { logPath: log } : {}), ...(key ? { trustedKey: key } : {}) });
|
|
62
|
+
if (!r.ok)
|
|
63
|
+
return { stdout: `${args[0]}: INVALID: ${r.problem}`, exitCode: 1 };
|
|
64
|
+
const checked = [`signed by ${receipt.signer}`, ...(key ? ["the expected key"] : []), ...(log ? ["matches the log"] : [])];
|
|
65
|
+
return { stdout: `${args[0]}: valid (${checked.join(", ")}): ${receipt.counts.calls} calls in session ${receipt.sessionId}`, exitCode: 0 };
|
|
66
|
+
}
|
|
67
|
+
if (cmd === "list") {
|
|
68
|
+
if (args.length > 1)
|
|
69
|
+
throw new Error(`Unexpected argument: ${args[1]}\n\n${RECEIPT_USAGE}`);
|
|
70
|
+
const path = await resolveAuditPath(args[0], config, RECEIPT_USAGE);
|
|
71
|
+
const sessions = listSessions(path);
|
|
72
|
+
if (sessions.length === 0)
|
|
73
|
+
return { stdout: `${path}: no sessions`, exitCode: 0 };
|
|
74
|
+
return { stdout: sessions.map((s) => `${s.last} ${s.sessionId} ${s.calls} call${s.calls === 1 ? "" : "s"}`).join("\n"), exitCode: 0 };
|
|
75
|
+
}
|
|
76
|
+
if (args.length > 1)
|
|
77
|
+
throw new Error(`Unexpected argument: ${args[1]}\n\n${RECEIPT_USAGE}`);
|
|
78
|
+
const path = await resolveAuditPath(args[0], config, RECEIPT_USAGE);
|
|
79
|
+
const receipt = buildReceipt(path, cmd, loadOrCreateSigner(defaultSigningKeyPath(env)));
|
|
80
|
+
const json = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
81
|
+
if (out) {
|
|
82
|
+
await writeFile(out, json);
|
|
83
|
+
return { stdout: `${out}: receipt for ${receipt.counts.calls} calls in session ${cmd}`, exitCode: 0 };
|
|
84
|
+
}
|
|
85
|
+
return { stdout: json.trimEnd(), exitCode: 0 };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=receiptCli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receiptCli.js","sourceRoot":"","sources":["../src/receiptCli.ts"],"names":[],"mappings":"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"}
|
package/dist/roles.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthorizationDecision, AuthorizationRequest, Authorizer } from "./authorizer.js";
|
|
2
|
+
import type { RoleConfig } from "./config.js";
|
|
3
|
+
export declare class RoleAuthorizer implements Authorizer {
|
|
4
|
+
readonly name = "roles";
|
|
5
|
+
private readonly roles;
|
|
6
|
+
constructor(roles: Record<string, RoleConfig>);
|
|
7
|
+
check(req: AuthorizationRequest): AuthorizationDecision;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=roles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAU9C,qBAAa,cAAe,YAAW,UAAU;IAC/C,QAAQ,CAAC,IAAI,WAAW;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC;IAU7C,KAAK,CAAC,GAAG,EAAE,oBAAoB,GAAG,qBAAqB;CAgBxD"}
|
package/dist/roles.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-based authorization from the config (`gateway.roles`):
|
|
3
|
+
*
|
|
4
|
+
* roles:
|
|
5
|
+
* maintainers: { members: [alice, bob], tools: ["github.*"] }
|
|
6
|
+
* readers: { members: ["*"], tools: ["github.read_*", "github.get_*"] }
|
|
7
|
+
*
|
|
8
|
+
* A call is allowed if a role the caller belongs to grants it: `tools` and
|
|
9
|
+
* `prompts` are `server.name` patterns (the same `*` wildcards as policy
|
|
10
|
+
* rules), `resources` are URI patterns. Everything else is denied. Members
|
|
11
|
+
* are principal ids; "*" means everyone, including an anonymous caller.
|
|
12
|
+
*/
|
|
13
|
+
import { globToRegExp } from "@adtl/labels";
|
|
14
|
+
export class RoleAuthorizer {
|
|
15
|
+
name = "roles";
|
|
16
|
+
roles;
|
|
17
|
+
constructor(roles) {
|
|
18
|
+
this.roles = Object.entries(roles).map(([name, r]) => ({
|
|
19
|
+
name,
|
|
20
|
+
members: new Set(r.members),
|
|
21
|
+
tools: r.tools.map(globToRegExp),
|
|
22
|
+
prompts: r.prompts.map(globToRegExp),
|
|
23
|
+
resources: r.resources.map(globToRegExp),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
check(req) {
|
|
27
|
+
const id = req.principal.id;
|
|
28
|
+
const [kind, what] = req.event === "tools/call"
|
|
29
|
+
? ["tools", `${req.server}.${req.tool}`]
|
|
30
|
+
: req.event === "prompts/get"
|
|
31
|
+
? ["prompts", `${req.server}.${req.prompt}`]
|
|
32
|
+
: ["resources", req.resource ?? ""];
|
|
33
|
+
const granted = this.roles.some((r) => (r.members.has("*") || (id !== undefined && r.members.has(id))) && r[kind].some((p) => p.test(what)));
|
|
34
|
+
if (granted)
|
|
35
|
+
return { allow: true };
|
|
36
|
+
const who = id ?? "an anonymous caller";
|
|
37
|
+
const noun = kind === "tools" ? "tool" : kind === "prompts" ? "prompt" : "resource";
|
|
38
|
+
return { allow: false, reason: `No role lets ${who} use the ${noun} ${what}` };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAY5C,MAAM,OAAO,cAAc;IAChB,IAAI,GAAG,OAAO,CAAC;IACP,KAAK,CAAiB;IAEvC,YAAY,KAAiC;QAC3C,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACrD,IAAI;YACJ,OAAO,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;YAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC;YAChC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;YACpC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC;SACzC,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAyB;QAC7B,MAAM,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAChB,GAAG,CAAC,KAAK,KAAK,YAAY;YACxB,CAAC,CAAE,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAW;YACnD,CAAC,CAAC,GAAG,CAAC,KAAK,KAAK,aAAa;gBAC3B,CAAC,CAAE,CAAC,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAW;gBACvD,CAAC,CAAE,CAAC,WAAW,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAW,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAC7B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAC5G,CAAC;QACF,IAAI,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,EAAE,IAAI,qBAAqB,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;QACpF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,GAAG,YAAY,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;IACjF,CAAC;CACF"}
|
package/dist/startup.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { type AdtlConfig } from "@adtl/labels";
|
|
3
3
|
import { ApprovalBroker } from "./approvals.js";
|
|
4
|
+
import type { Principal } from "./authorizer.js";
|
|
4
5
|
import { type GatewayConfig, type Upstream } from "./config.js";
|
|
5
6
|
import { AdtlGateway, type GatewayLogger } from "./gateway.js";
|
|
6
7
|
export declare class GatewayConfigError extends Error {
|
|
@@ -19,6 +20,14 @@ export declare function bundledInterceptorsCli(): string;
|
|
|
19
20
|
export declare function connectUpstream(id: string, u: Upstream, baseDir: string): Promise<Client>;
|
|
20
21
|
/** Build the approval broker from `gateway.approvals`, or undefined if not configured. */
|
|
21
22
|
export declare function createApprovals(cfg: GatewayConfig, env: NodeJS.ProcessEnv, log: GatewayLogger): ApprovalBroker | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Check that every role a policy rule names under `approvers:` exists, has
|
|
25
|
+
* named members, and that each member has a channel in
|
|
26
|
+
* `gateway.approvals.approvers`. Returns each such role's members.
|
|
27
|
+
*/
|
|
28
|
+
export declare function approverRoles(labels: AdtlConfig, cfg: GatewayConfig): Map<string, string[]>;
|
|
29
|
+
/** `gateway.principal`, with `${VAR}` in its id expanded. */
|
|
30
|
+
export declare function gatewayPrincipal(cfg: GatewayConfig, env: NodeJS.ProcessEnv): Principal | undefined;
|
|
22
31
|
/**
|
|
23
32
|
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
24
33
|
* servers, the gateway uses that id and keeps session labels in files, so the
|
package/dist/startup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,
|
|
1
|
+
{"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,EAAuC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAGpF,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAML,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAKhF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3E;AAED,8GAA8G;AAC9G,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAqB/E;AAED,iDAAiD;AACjD,wBAAgB,sBAAsB,IAAI,MAAM,CAI/C;AAaD,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO/F;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,cAAc,GAAG,SAAS,CAwB1H;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAqB3F;AAED,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,SAAS,GAAG,SAAS,CAMlG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAGpG;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAO,GAC7D,OAAO,CAAC,cAAc,CAAC,CAyFzB"}
|
package/dist/startup.js
CHANGED
|
@@ -9,16 +9,17 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
9
9
|
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
10
10
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
11
|
import { INTERCEPTORS_EXTENSION, LABEL_INTERCEPTOR, POLICY_INTERCEPTOR, parseConfigText } from "@adtl/interceptors";
|
|
12
|
-
import { defaultSessionsDir } from "@adtl/labels";
|
|
12
|
+
import { SIGNING_KEY_ENV, defaultSessionsDir } from "@adtl/labels";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { ZodError } from "zod";
|
|
15
15
|
import { ApprovalBroker } from "./approvals.js";
|
|
16
16
|
import { JsonlAuditLog } from "./audit.js";
|
|
17
|
-
import { GatewayConfigSchema, expandApprovals, expandUpstream, isHttpUpstream, } from "./config.js";
|
|
17
|
+
import { GatewayConfigSchema, expandApprovals, expandEnv, expandUpstream, isHttpUpstream, } from "./config.js";
|
|
18
18
|
import { EmailNotifier } from "./email.js";
|
|
19
19
|
import { AdtlGateway, GATEWAY_VERSION } from "./gateway.js";
|
|
20
20
|
import { InterceptorInvoker } from "./invoker.js";
|
|
21
21
|
import { NtfyNotifier } from "./ntfy.js";
|
|
22
|
+
import { RoleAuthorizer } from "./roles.js";
|
|
22
23
|
export class GatewayConfigError extends Error {
|
|
23
24
|
constructor(message) {
|
|
24
25
|
super(message);
|
|
@@ -100,12 +101,57 @@ export function createApprovals(cfg, env, log) {
|
|
|
100
101
|
if (a.email) {
|
|
101
102
|
notifiers.push(new EmailNotifier({ smtp: a.email.smtp, from: a.email.from, to: [a.email.to].flat(), on: a.email.on }));
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
const approverChannels = new Map(Object.entries(a.approvers ?? {}).map(([id, c]) => [id, [new NtfyNotifier({ ...c.ntfy, name: `ntfy:${id}` })]]));
|
|
105
|
+
if (notifiers.length === 0 && approverChannels.size === 0 && !a.elicitation)
|
|
104
106
|
return undefined;
|
|
105
|
-
if (!notifiers.some((n) => n.listen))
|
|
107
|
+
if (!notifiers.some((n) => n.listen) && approverChannels.size === 0 && !a.elicitation) {
|
|
106
108
|
log.warn('no approval channel can carry answers (configure gateway.approvals.ntfy); "ask" still blocks');
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
}
|
|
110
|
+
const approvers = approverChannels.size ? `; approvers: ${[...approverChannels.keys()].join(", ")}` : "";
|
|
111
|
+
const shared = [...notifiers.map((n) => n.name), ...(a.elicitation ? ["elicitation (if the client supports it)"] : [])];
|
|
112
|
+
log.info(`approvals: ${shared.join(", ") || "none shared"}${approvers} (timeout ${a.timeoutSeconds}s)`);
|
|
113
|
+
return new ApprovalBroker({ notifiers, approverChannels, timeoutMs: a.timeoutSeconds * 1000, log });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check that every role a policy rule names under `approvers:` exists, has
|
|
117
|
+
* named members, and that each member has a channel in
|
|
118
|
+
* `gateway.approvals.approvers`. Returns each such role's members.
|
|
119
|
+
*/
|
|
120
|
+
export function approverRoles(labels, cfg) {
|
|
121
|
+
const out = new Map();
|
|
122
|
+
const problems = [];
|
|
123
|
+
for (const rule of labels.policy.rules) {
|
|
124
|
+
for (const role of rule.approvers ?? []) {
|
|
125
|
+
if (out.has(role))
|
|
126
|
+
continue;
|
|
127
|
+
const r = cfg.roles?.[role];
|
|
128
|
+
if (!r) {
|
|
129
|
+
problems.push(`policy rule "${rule.name}" names approver role "${role}", which is not under gateway.roles`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const members = [...r.members];
|
|
133
|
+
if (members.includes("*"))
|
|
134
|
+
problems.push(`approver role "${role}" has member "*": approvers must be named, so the answer says who approved`);
|
|
135
|
+
for (const m of members.filter((x) => x !== "*")) {
|
|
136
|
+
if (!cfg.approvals?.approvers?.[m])
|
|
137
|
+
problems.push(`approver "${m}" (role "${role}") has no channel under gateway.approvals.approvers`);
|
|
138
|
+
}
|
|
139
|
+
out.set(role, members);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (problems.length)
|
|
143
|
+
throw new GatewayConfigError(`approvers:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/** `gateway.principal`, with `${VAR}` in its id expanded. */
|
|
147
|
+
export function gatewayPrincipal(cfg, env) {
|
|
148
|
+
const p = cfg.principal;
|
|
149
|
+
if (!p)
|
|
150
|
+
return undefined;
|
|
151
|
+
const id = expandEnv(p.id, env, "gateway.principal.id").trim();
|
|
152
|
+
if (!id)
|
|
153
|
+
throw new GatewayConfigError("gateway.principal.id is empty after expanding environment variables");
|
|
154
|
+
return { type: p.type, id, ...(p.claims ? { claims: p.claims } : {}) };
|
|
109
155
|
}
|
|
110
156
|
/**
|
|
111
157
|
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
@@ -118,6 +164,8 @@ export function sharedSession(env) {
|
|
|
118
164
|
}
|
|
119
165
|
export async function startGateway(loaded, opts = {}) {
|
|
120
166
|
const env = opts.env ?? process.env;
|
|
167
|
+
// Config problems that need no process: fail before starting any.
|
|
168
|
+
const approverMembers = approverRoles(loaded.labels, loaded.gateway);
|
|
121
169
|
const baseDir = dirname(loaded.path);
|
|
122
170
|
const log = opts.logger ?? { info: (m) => console.error(`[adtl-gateway] ${m}`), warn: (m) => console.error(`[adtl-gateway] warning: ${m}`) };
|
|
123
171
|
const clients = [];
|
|
@@ -127,7 +175,10 @@ export async function startGateway(loaded, opts = {}) {
|
|
|
127
175
|
const shared = sharedSession(env);
|
|
128
176
|
const custom = loaded.gateway.interceptors && expandUpstream("interceptors", loaded.gateway.interceptors, env);
|
|
129
177
|
// The interceptor process starts with a minimal environment; pass on where the ledger lives.
|
|
130
|
-
const ledgerEnv =
|
|
178
|
+
const ledgerEnv = {
|
|
179
|
+
...(env.ADTL_LEDGER ? { ADTL_LEDGER: env.ADTL_LEDGER } : {}),
|
|
180
|
+
...(env[SIGNING_KEY_ENV] ? { [SIGNING_KEY_ENV]: env[SIGNING_KEY_ENV] } : {}),
|
|
181
|
+
};
|
|
131
182
|
const icfg = custom
|
|
132
183
|
? { ...custom, env: { ...ledgerEnv, ...custom.env, ...(shared ? { ADTL_SESSIONS_DIR: shared.dir } : {}) } }
|
|
133
184
|
: {
|
|
@@ -176,12 +227,23 @@ export async function startGateway(loaded, opts = {}) {
|
|
|
176
227
|
log.info(`upstreams: ${[...upstreams.keys()].join(", ")}`);
|
|
177
228
|
const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
|
|
178
229
|
const approvals = createApprovals(loaded.gateway, env, log);
|
|
230
|
+
const principal = gatewayPrincipal(loaded.gateway, env);
|
|
231
|
+
const roles = loaded.gateway.roles;
|
|
232
|
+
if (roles) {
|
|
233
|
+
log.info(`roles: ${Object.keys(roles).join(", ")} (caller: ${principal?.id ?? "anonymous"})`);
|
|
234
|
+
if (!principal)
|
|
235
|
+
log.warn('roles are set but gateway.principal is not: only roles with members ["*"] apply');
|
|
236
|
+
}
|
|
179
237
|
const gateway = new AdtlGateway({
|
|
180
238
|
...(shared ? { sessionId: shared.sessionId } : {}),
|
|
181
239
|
upstreams,
|
|
182
240
|
invoker,
|
|
183
241
|
...(auditLog ? { audit: auditLog } : {}),
|
|
184
242
|
...(approvals ? { approvals } : {}),
|
|
243
|
+
...(roles ? { authorizer: new RoleAuthorizer(roles) } : {}),
|
|
244
|
+
...(principal ? { principal } : {}),
|
|
245
|
+
...(approverMembers.size ? { approverRoles: approverMembers } : {}),
|
|
246
|
+
...(loaded.gateway.approvals?.elicitation ? { elicitApprovals: true } : {}),
|
|
185
247
|
logger: log,
|
|
186
248
|
});
|
|
187
249
|
return { gateway, close: closeAll };
|