@adtl/gateway 0.1.0 → 0.2.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.
Files changed (49) hide show
  1. package/README.md +48 -4
  2. package/dist/approvals.d.ts +97 -0
  3. package/dist/approvals.d.ts.map +1 -0
  4. package/dist/approvals.js +151 -0
  5. package/dist/approvals.js.map +1 -0
  6. package/dist/audit.d.ts +10 -0
  7. package/dist/audit.d.ts.map +1 -1
  8. package/dist/audit.js.map +1 -1
  9. package/dist/cli.js +8 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +352 -2
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +69 -2
  14. package/dist/config.js.map +1 -1
  15. package/dist/email.d.ts +35 -0
  16. package/dist/email.d.ts.map +1 -0
  17. package/dist/email.js +63 -0
  18. package/dist/email.js.map +1 -0
  19. package/dist/gateway.d.ts +12 -2
  20. package/dist/gateway.d.ts.map +1 -1
  21. package/dist/gateway.js +91 -11
  22. package/dist/gateway.js.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/invoker.d.ts +10 -1
  28. package/dist/invoker.d.ts.map +1 -1
  29. package/dist/invoker.js +15 -6
  30. package/dist/invoker.js.map +1 -1
  31. package/dist/ntfy.d.ts +44 -0
  32. package/dist/ntfy.d.ts.map +1 -0
  33. package/dist/ntfy.js +133 -0
  34. package/dist/ntfy.js.map +1 -0
  35. package/dist/startup.d.ts +3 -0
  36. package/dist/startup.d.ts.map +1 -1
  37. package/dist/startup.js +39 -4
  38. package/dist/startup.js.map +1 -1
  39. package/package.json +4 -3
  40. package/src/approvals.ts +219 -0
  41. package/src/audit.ts +5 -0
  42. package/src/cli.ts +15 -4
  43. package/src/config.ts +71 -2
  44. package/src/email.ts +79 -0
  45. package/src/gateway.ts +103 -11
  46. package/src/index.ts +3 -0
  47. package/src/invoker.ts +17 -5
  48. package/src/ntfy.ts +154 -0
  49. package/src/startup.ts +36 -3
package/dist/startup.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AdtlConfig } from "@adtl/labels";
2
+ import { ApprovalBroker } from "./approvals.js";
2
3
  import { type GatewayConfig } from "./config.js";
3
4
  import { AdtlGateway, type GatewayLogger } from "./gateway.js";
4
5
  export declare class GatewayConfigError extends Error {
@@ -12,6 +13,8 @@ export interface LoadedConfig {
12
13
  export declare function loadGatewayConfig(path: string): Promise<LoadedConfig>;
13
14
  /** Path of the bundled adtl-interceptors CLI. */
14
15
  export declare function bundledInterceptorsCli(): string;
16
+ /** Build the approval broker from `gateway.approvals`, or undefined if not configured. */
17
+ export declare function createApprovals(cfg: GatewayConfig, env: NodeJS.ProcessEnv, log: GatewayLogger): ApprovalBroker | undefined;
15
18
  export interface StartedGateway {
16
19
  gateway: AdtlGateway;
17
20
  close(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAGhF,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,CA2B3E;AAED,iDAAiD;AACjD,wBAAgB,sBAAsB,IAAI,MAAM,CAI/C;AAsBD,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,CAuDzB"}
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAKL,KAAK,aAAa,EAGnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAIhF,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,CA2B3E;AAED,iDAAiD;AACjD,wBAAgB,sBAAsB,IAAI,MAAM,CAI/C;AAsBD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,cAAc,GAAG,SAAS,CAiB1H;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,CAgEzB"}
package/dist/startup.js CHANGED
@@ -11,10 +11,13 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
11
11
  import { INTERCEPTORS_EXTENSION, LABEL_INTERCEPTOR, POLICY_INTERCEPTOR, parseConfigText } from "@adtl/interceptors";
12
12
  import { parse as parseYaml } from "yaml";
13
13
  import { ZodError } from "zod";
14
+ import { ApprovalBroker } from "./approvals.js";
14
15
  import { JsonlAuditLog } from "./audit.js";
15
- import { GatewayConfigSchema, expandUpstream, isHttpUpstream, } from "./config.js";
16
+ import { GatewayConfigSchema, expandApprovals, expandUpstream, isHttpUpstream, } from "./config.js";
17
+ import { EmailNotifier } from "./email.js";
16
18
  import { AdtlGateway, GATEWAY_VERSION } from "./gateway.js";
17
19
  import { InterceptorInvoker } from "./invoker.js";
20
+ import { NtfyNotifier } from "./ntfy.js";
18
21
  export class GatewayConfigError extends Error {
19
22
  constructor(message) {
20
23
  super(message);
@@ -76,6 +79,28 @@ async function connect(id, u, baseDir) {
76
79
  await client.connect(transport);
77
80
  return client;
78
81
  }
82
+ /** Build the approval broker from `gateway.approvals`, or undefined if not configured. */
83
+ export function createApprovals(cfg, env, log) {
84
+ if (!cfg.approvals)
85
+ return undefined;
86
+ const a = expandApprovals(cfg.approvals, env);
87
+ const notifiers = [];
88
+ if (a.ntfy) {
89
+ if (/^https:\/\/ntfy\.sh\/?$/.test(a.ntfy.server) && a.ntfy.topic.length < 20 && !a.ntfy.token) {
90
+ log.warn("gateway.approvals.ntfy.topic is short and on the public ntfy.sh server: anyone who guesses it can approve calls. Use a long random topic.");
91
+ }
92
+ notifiers.push(new NtfyNotifier(a.ntfy));
93
+ }
94
+ if (a.email) {
95
+ notifiers.push(new EmailNotifier({ smtp: a.email.smtp, from: a.email.from, to: [a.email.to].flat(), on: a.email.on }));
96
+ }
97
+ if (notifiers.length === 0)
98
+ return undefined;
99
+ if (!notifiers.some((n) => n.listen))
100
+ log.warn('no approval channel can carry answers (configure gateway.approvals.ntfy); "ask" still blocks');
101
+ log.info(`approvals: ${notifiers.map((n) => n.name).join(", ")} (timeout ${a.timeoutSeconds}s)`);
102
+ return new ApprovalBroker({ notifiers, timeoutMs: a.timeoutSeconds * 1000, log });
103
+ }
79
104
  export async function startGateway(loaded, opts = {}) {
80
105
  const env = opts.env ?? process.env;
81
106
  const baseDir = dirname(loaded.path);
@@ -99,8 +124,11 @@ export async function startGateway(loaded, opts = {}) {
99
124
  throw new GatewayConfigError(`The interceptor server does not offer ${required}; refusing to start without it`);
100
125
  }
101
126
  }
102
- const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs);
127
+ const audit = loaded.gateway.mode === "audit";
128
+ const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined);
103
129
  log.info(`interceptors: ${names.join(", ")}`);
130
+ if (audit)
131
+ log.warn("audit mode: calls are logged but never stopped or changed");
104
132
  // 2. Upstream servers. One failing upstream does not stop the others.
105
133
  const upstreams = new Map();
106
134
  const ids = Object.keys(loaded.gateway.upstreams);
@@ -121,8 +149,15 @@ export async function startGateway(loaded, opts = {}) {
121
149
  if (upstreams.size === 0)
122
150
  throw new GatewayConfigError("No upstream server could be connected");
123
151
  log.info(`upstreams: ${[...upstreams.keys()].join(", ")}`);
124
- const audit = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
125
- const gateway = new AdtlGateway({ upstreams, invoker, ...(audit ? { audit } : {}), logger: log });
152
+ const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
153
+ const approvals = createApprovals(loaded.gateway, env, log);
154
+ const gateway = new AdtlGateway({
155
+ upstreams,
156
+ invoker,
157
+ ...(auditLog ? { audit: auditLog } : {}),
158
+ ...(approvals ? { approvals } : {}),
159
+ logger: log,
160
+ });
126
161
  return { gateway, close: closeAll };
127
162
  }
128
163
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"startup.js","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"startup.js","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adtl/gateway",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Experimental: MCP gateway that labels every tool result and blocks risky tool calls using ADTL trust labels",
5
5
  "keywords": [
6
6
  "mcp",
@@ -46,9 +46,10 @@
46
46
  "url": "https://github.com/probita-ai/adtl/issues"
47
47
  },
48
48
  "dependencies": {
49
- "@adtl/interceptors": "0.1.0",
50
- "@adtl/labels": "0.1.0",
49
+ "@adtl/interceptors": "0.2.0",
50
+ "@adtl/labels": "0.2.0",
51
51
  "@modelcontextprotocol/sdk": "1.31.0",
52
+ "nodemailer": "10.0.12",
52
53
  "yaml": "2.9.1",
53
54
  "zod": "3.25.76"
54
55
  },
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Human approvals for "ask" decisions.
3
+ *
4
+ * When adtl.policy answers `ask`, the gateway holds the call and asks the
5
+ * broker. The broker sends the request to every notifier and waits for an
6
+ * answer from a notifier that can carry one (e.g. ntfy). No answer before the
7
+ * timeout means deny.
8
+ *
9
+ * Answers are signed tokens: HMAC over the approval id, the call's hash, the
10
+ * decision and an expiry. A token only ever answers the one pending call it
11
+ * was made for, and only once. Anyone who can read the push channel can still
12
+ * use the tokens sent on it, so that channel must stay private.
13
+ */
14
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
15
+
16
+ export type ApprovalDecision = "allow" | "deny";
17
+ export type ApprovalOutcome = "approved" | "denied" | "timeout" | "undeliverable";
18
+
19
+ export interface ApprovalRequest {
20
+ id: string;
21
+ sessionId: string;
22
+ server: string;
23
+ tool: string;
24
+ callHash: string;
25
+ reason: string;
26
+ matched: string[];
27
+ /** The session label that triggered the rule (no arguments or results). */
28
+ sessionLabel?: unknown;
29
+ createdAt: Date;
30
+ expiresAt: Date;
31
+ }
32
+
33
+ export interface ApprovalResult {
34
+ outcome: ApprovalOutcome;
35
+ /** Notifier that delivered the answer. */
36
+ channel?: string;
37
+ approver?: string;
38
+ waitedMs: number;
39
+ }
40
+
41
+ export type ApprovalEvent =
42
+ | { kind: "pending"; request: ApprovalRequest; tokens: Record<ApprovalDecision, string> }
43
+ | { kind: "resolved"; request: ApprovalRequest; result: ApprovalResult }
44
+ | { kind: "blocked"; sessionId: string; server: string; tool: string; reason: string; matched: string[] };
45
+
46
+ /** Called by a notifier with every answer it receives. Returns true if the answer was accepted. */
47
+ export type AnswerFn = (token: string, approver?: string) => boolean;
48
+
49
+ export interface Notifier {
50
+ readonly name: string;
51
+ /** Deliver an event. Throwing marks the delivery as failed; it never stops the call by itself. */
52
+ notify(event: ApprovalEvent): Promise<void>;
53
+ /**
54
+ * Notifiers that can carry answers: pass every answer for `request` to
55
+ * `answer` until `signal` aborts. Called before the pending event is sent.
56
+ */
57
+ listen?(request: ApprovalRequest, answer: AnswerFn, signal: AbortSignal): Promise<void>;
58
+ }
59
+
60
+ const TOKEN_PREFIX = "adtl1";
61
+
62
+ /** Error message plus its cause, e.g. "fetch failed (ECONNRESET)". */
63
+ export function errorText(err: unknown): string {
64
+ if (!(err instanceof Error)) return String(err);
65
+ const cause = err.cause as { code?: string; message?: string } | undefined;
66
+ const detail = cause?.code ?? cause?.message;
67
+ return detail && !err.message.includes(detail) ? `${err.message} (${detail})` : err.message;
68
+ }
69
+
70
+ export class ApprovalTokens {
71
+ constructor(private readonly secret: Buffer = randomBytes(32)) {}
72
+
73
+ sign(request: Pick<ApprovalRequest, "id" | "callHash" | "expiresAt">, decision: ApprovalDecision): string {
74
+ const exp = Math.floor(request.expiresAt.getTime() / 1000);
75
+ return `${TOKEN_PREFIX}.${request.id}.${decision}.${exp}.${this.mac(request.id, request.callHash, decision, exp)}`;
76
+ }
77
+
78
+ /** Parse a token without verifying it. */
79
+ static parse(token: string): { id: string; decision: ApprovalDecision; exp: number; sig: string } | undefined {
80
+ const parts = token.trim().split(".");
81
+ if (parts.length !== 5 || parts[0] !== TOKEN_PREFIX) return undefined;
82
+ const [, id, decision, exp, sig] = parts as [string, string, string, string, string];
83
+ if (decision !== "allow" && decision !== "deny") return undefined;
84
+ if (!/^\d+$/.test(exp)) return undefined;
85
+ return { id, decision, exp: Number(exp), sig };
86
+ }
87
+
88
+ /** Verify a token against the pending request it claims to answer. */
89
+ verify(token: string, request: Pick<ApprovalRequest, "id" | "callHash">, now: Date): ApprovalDecision | undefined {
90
+ const t = ApprovalTokens.parse(token);
91
+ if (!t || t.id !== request.id) return undefined;
92
+ if (now.getTime() > t.exp * 1000) return undefined;
93
+ const expected = Buffer.from(this.mac(t.id, request.callHash, t.decision, t.exp));
94
+ const given = Buffer.from(t.sig);
95
+ if (given.length !== expected.length || !timingSafeEqual(given, expected)) return undefined;
96
+ return t.decision;
97
+ }
98
+
99
+ private mac(id: string, callHash: string, decision: ApprovalDecision, exp: number): string {
100
+ return createHmac("sha256", this.secret).update(`${id}\n${callHash}\n${decision}\n${exp}`).digest("base64url");
101
+ }
102
+ }
103
+
104
+ export interface ApprovalBrokerOptions {
105
+ notifiers: Notifier[];
106
+ timeoutMs: number;
107
+ tokens?: ApprovalTokens;
108
+ now?: () => Date;
109
+ log?: { warn(msg: string): void };
110
+ }
111
+
112
+ interface Pending {
113
+ request: ApprovalRequest;
114
+ settle(decision: ApprovalDecision, channel: string, approver?: string): void;
115
+ }
116
+
117
+ export class ApprovalBroker {
118
+ private readonly notifiers: Notifier[];
119
+ private readonly timeoutMs: number;
120
+ private readonly tokens: ApprovalTokens;
121
+ private readonly now: () => Date;
122
+ private readonly log: { warn(msg: string): void };
123
+ private readonly pending = new Map<string, Pending>();
124
+
125
+ constructor(opts: ApprovalBrokerOptions) {
126
+ this.notifiers = opts.notifiers;
127
+ this.timeoutMs = opts.timeoutMs;
128
+ this.tokens = opts.tokens ?? new ApprovalTokens();
129
+ this.now = opts.now ?? (() => new Date());
130
+ this.log = opts.log ?? { warn: () => {} };
131
+ }
132
+
133
+ /** True if at least one notifier can carry answers. Without one, "ask" stays a block. */
134
+ get canApprove(): boolean {
135
+ return this.notifiers.some((n) => n.listen);
136
+ }
137
+
138
+ /** Ask for approval and wait for the answer, the timeout, or a failure to reach anyone. */
139
+ async request(input: Omit<ApprovalRequest, "id" | "createdAt" | "expiresAt">): Promise<ApprovalResult> {
140
+ const createdAt = this.now();
141
+ const request: ApprovalRequest = {
142
+ ...input,
143
+ id: randomUUID(),
144
+ createdAt,
145
+ expiresAt: new Date(createdAt.getTime() + this.timeoutMs),
146
+ };
147
+ const started = performance.now();
148
+ const waited = () => Math.round(performance.now() - started);
149
+ const abort = new AbortController();
150
+
151
+ let finish!: (r: ApprovalResult) => void;
152
+ const result = new Promise<ApprovalResult>((resolve) => {
153
+ const timer = setTimeout(() => finish({ outcome: "timeout", waitedMs: waited() }), this.timeoutMs);
154
+ finish = (r) => {
155
+ if (!this.pending.delete(request.id)) return; // already settled
156
+ clearTimeout(timer);
157
+ abort.abort();
158
+ resolve(r);
159
+ };
160
+ });
161
+ this.pending.set(request.id, {
162
+ request,
163
+ settle: (decision, channel, approver) =>
164
+ finish({ outcome: decision === "allow" ? "approved" : "denied", channel, ...(approver ? { approver } : {}), waitedMs: waited() }),
165
+ });
166
+
167
+ // Start listening before anyone can answer, then send the request everywhere.
168
+ const listeners = this.notifiers.filter((n) => n.listen);
169
+ for (const n of listeners) {
170
+ n.listen!(request, (token, approver) => this.answer(token, n.name, approver), abort.signal).catch((err: unknown) =>
171
+ this.log.warn(`${n.name}: stopped listening for approvals: ${errorText(err)}`),
172
+ );
173
+ }
174
+ const tokens = { allow: this.tokens.sign(request, "allow"), deny: this.tokens.sign(request, "deny") };
175
+ const sent = this.notifiers.map((n) =>
176
+ n.notify({ kind: "pending", request, tokens }).then(
177
+ () => true,
178
+ (err: unknown) => {
179
+ this.log.warn(`${n.name}: could not send approval request: ${errorText(err)}`);
180
+ return false;
181
+ },
182
+ ),
183
+ );
184
+ // Nobody who could answer was reached: deny now rather than wait out the timeout.
185
+ // Notify-only channels (email) are not waited for.
186
+ void Promise.all(sent.filter((_, i) => this.notifiers[i]!.listen)).then((ok) => {
187
+ if (!ok.includes(true)) finish({ outcome: "undeliverable", waitedMs: waited() });
188
+ });
189
+
190
+ const r = await result;
191
+ this.broadcast({ kind: "resolved", request, result: r });
192
+ return r;
193
+ }
194
+
195
+ /** Handle an answer token from a channel. Returns true if it settled a pending approval. */
196
+ answer(token: string, channel: string, approver?: string): boolean {
197
+ const parsed = ApprovalTokens.parse(token);
198
+ const p = parsed && this.pending.get(parsed.id);
199
+ if (!p) return false; // not ours, already answered, or garbage
200
+ const decision = this.tokens.verify(token, p.request, this.now());
201
+ if (!decision) {
202
+ this.log.warn(`${channel}: ignored an approval answer with a bad or expired signature`);
203
+ return false;
204
+ }
205
+ p.settle(decision, channel, approver);
206
+ return true;
207
+ }
208
+
209
+ /** Tell notifiers a call was blocked. Fire and forget. */
210
+ blocked(event: Omit<Extract<ApprovalEvent, { kind: "blocked" }>, "kind">): void {
211
+ this.broadcast({ kind: "blocked", ...event });
212
+ }
213
+
214
+ private broadcast(event: ApprovalEvent): void {
215
+ for (const n of this.notifiers) {
216
+ n.notify(event).catch((err: unknown) => this.log.warn(`${n.name}: could not send ${event.kind} notice: ${errorText(err)}`));
217
+ }
218
+ }
219
+ }
package/src/audit.ts CHANGED
@@ -15,7 +15,12 @@ export interface AuditEntry {
15
15
  tool: string;
16
16
  /** Final outcome of the call. */
17
17
  outcome: "forwarded" | "blocked" | "upstream_error";
18
+ /** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
18
19
  decision?: string;
20
+ /** Present when the gateway ran in audit mode: nothing was stopped or changed. */
21
+ mode?: "audit";
22
+ /** Present when a human was asked: outcome, channel, approver (if known) and wait time. */
23
+ approval?: { outcome: string; channel?: string; approver?: string; waitedMs: number };
19
24
  reason?: string;
20
25
  matched?: unknown;
21
26
  callHash?: unknown;
package/src/cli.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * adtl-gateway: an MCP server for your agent that proxies to your real MCP
4
4
  * servers through ADTL's interceptors.
5
5
  *
6
- * adtl-gateway --config adtl.yaml
6
+ * adtl-gateway --config adtl.yaml [--audit]
7
7
  *
8
8
  * Point your MCP client (e.g. Claude Code's .mcp.json) at this command.
9
9
  * Logs go to stderr; stdout carries MCP JSON-RPC only.
@@ -13,24 +13,34 @@ import { EnvExpansionError } from "./config.js";
13
13
  import { GATEWAY_VERSION } from "./gateway.js";
14
14
  import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
15
15
 
16
- const USAGE = `Usage: adtl-gateway --config <adtl.yaml>
16
+ const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
17
17
 
18
18
  Runs an MCP server over stdio that forwards tool calls to the upstream
19
19
  servers in the config, checking and labeling each call with ADTL.
20
20
 
21
21
  Options:
22
22
  -c, --config <path> ADTL config file with a gateway: section. Required.
23
+ --audit Log what would be stopped, but stop nothing
24
+ (same as gateway.mode: audit).
23
25
  -v, --version Print the version and exit.
24
26
  -h, --help Show this help.`;
25
27
 
26
- function parseArgs(argv: string[]): { config?: string; help?: boolean; version?: boolean } {
27
- const out: { config?: string; help?: boolean; version?: boolean } = {};
28
+ interface Args {
29
+ config?: string;
30
+ audit?: boolean;
31
+ help?: boolean;
32
+ version?: boolean;
33
+ }
34
+
35
+ function parseArgs(argv: string[]): Args {
36
+ const out: Args = {};
28
37
  for (let i = 0; i < argv.length; i++) {
29
38
  const a = argv[i];
30
39
  if (a === "-h" || a === "--help") out.help = true;
31
40
  else if (a === "-v" || a === "--version") out.version = true;
32
41
  else if (a === "-c" || a === "--config") out.config = argv[++i];
33
42
  else if (a?.startsWith("--config=")) out.config = a.slice("--config=".length);
43
+ else if (a === "--audit") out.audit = true;
34
44
  else throw new GatewayConfigError(`Unknown argument: ${a}\n\n${USAGE}`);
35
45
  }
36
46
  return out;
@@ -44,6 +54,7 @@ async function main(): Promise<void> {
44
54
  if (!args.config) throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
45
55
 
46
56
  const loaded = await loadGatewayConfig(args.config);
57
+ if (args.audit) loaded.gateway.mode = "audit";
47
58
  const started = await startGateway(loaded);
48
59
  const server = started.gateway.createServer();
49
60
 
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * gateway:
5
5
  * audit: ./adtl-audit.jsonl # optional JSONL audit log
6
+ * mode: audit # optional: log what would be stopped, stop nothing
6
7
  * upstreams:
7
8
  * github: # id must match `servers.github` in the label config
8
9
  * command: npx
@@ -11,11 +12,19 @@
11
12
  * docs:
12
13
  * url: https://example.com/mcp
13
14
  * headers: { Authorization: "Bearer ${DOCS_TOKEN}" }
15
+ * approvals: # optional: make "ask" really ask
16
+ * timeoutSeconds: 50
17
+ * ntfy: { topic: "${ADTL_NTFY_TOPIC}" }
18
+ * email:
19
+ * smtp: { host: smtp.example.com, port: 465, user: "${SMTP_USER}", pass: "${SMTP_PASS}" }
20
+ * from: adtl@example.com
21
+ * to: you@example.com
14
22
  *
15
- * `${VAR}` in command, args, env, cwd, url and headers is replaced from the
16
- * gateway's environment. A missing variable is a config error.
23
+ * `${VAR}` in command, args, env, cwd, url, headers and approval settings is
24
+ * replaced from the gateway's environment. A missing variable is a config error.
17
25
  */
18
26
  import { z } from "zod";
27
+ import { EMAIL_EVENTS } from "./email.js";
19
28
 
20
29
  const UPSTREAM_ID = /^[A-Za-z0-9-]+$/;
21
30
 
@@ -40,6 +49,48 @@ export type StdioUpstream = z.infer<typeof StdioUpstreamSchema>;
40
49
  export type HttpUpstream = z.infer<typeof HttpUpstreamSchema>;
41
50
  export type Upstream = z.infer<typeof UpstreamSchema>;
42
51
 
52
+ export const NtfyConfigSchema = z
53
+ .object({
54
+ server: z.string().min(1).default("https://ntfy.sh"),
55
+ /** Topic approval requests are sent to. Anyone who can read it can approve: keep it secret. */
56
+ topic: z.string().min(1),
57
+ /** Topic answers are posted to. Default: `<topic>-reply`. */
58
+ replyTopic: z.string().min(1).optional(),
59
+ token: z.string().min(1).optional(),
60
+ })
61
+ .strict();
62
+
63
+ export const EmailConfigSchema = z
64
+ .object({
65
+ smtp: z
66
+ .object({
67
+ host: z.string().min(1),
68
+ port: z.number().int().positive().default(587),
69
+ /** TLS from the start. Default: true on port 465, otherwise STARTTLS. */
70
+ secure: z.boolean().optional(),
71
+ user: z.string().min(1).optional(),
72
+ pass: z.string().optional(),
73
+ })
74
+ .strict(),
75
+ from: z.string().min(1),
76
+ to: z.union([z.string().min(1), z.array(z.string().min(1)).nonempty()]),
77
+ on: z.array(z.enum(EMAIL_EVENTS)).nonempty().default(["pending", "blocked"]),
78
+ })
79
+ .strict();
80
+
81
+ export const ApprovalsConfigSchema = z
82
+ .object({
83
+ /**
84
+ * How long a call waits for an answer; no answer means deny. Many MCP
85
+ * clients give up on a request after 60 seconds, hence the default.
86
+ */
87
+ timeoutSeconds: z.number().int().positive().max(3600).default(50),
88
+ ntfy: NtfyConfigSchema.optional(),
89
+ email: EmailConfigSchema.optional(),
90
+ })
91
+ .strict();
92
+ export type ApprovalsConfig = z.infer<typeof ApprovalsConfigSchema>;
93
+
43
94
  export const GatewayConfigSchema = z
44
95
  .object({
45
96
  upstreams: z
@@ -52,6 +103,13 @@ export const GatewayConfigSchema = z
52
103
  interceptors: StdioUpstreamSchema.optional(),
53
104
  /** Path of a JSONL audit log. Relative paths are resolved against the config file's directory. */
54
105
  audit: z.string().min(1).optional(),
106
+ /**
107
+ * `active` (default) enforces the policy. `audit` stops and changes nothing, and
108
+ * logs what would have been stopped: for trying a policy out safely.
109
+ */
110
+ mode: z.enum(["active", "audit"]).default("active"),
111
+ /** Human approvals for "ask" decisions. Without an answering channel, "ask" blocks. */
112
+ approvals: ApprovalsConfigSchema.optional(),
55
113
  /** Timeout for each interceptor invocation, in milliseconds. */
56
114
  interceptorTimeoutMs: z.number().int().positive().default(5000),
57
115
  })
@@ -95,6 +153,17 @@ export function expandUpstream<T extends Upstream>(id: string, u: T, env: NodeJS
95
153
  };
96
154
  }
97
155
 
156
+ /** Expand `${VAR}` in every string of the approvals section. */
157
+ export function expandApprovals(a: ApprovalsConfig, env: NodeJS.ProcessEnv): ApprovalsConfig {
158
+ const walk = (v: unknown, where: string): unknown => {
159
+ if (typeof v === "string") return expandEnv(v, env, where);
160
+ if (Array.isArray(v)) return v.map((x, i) => walk(x, `${where}[${i}]`));
161
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, `${where}.${k}`)]));
162
+ return v;
163
+ };
164
+ return walk(a, "gateway.approvals") as ApprovalsConfig;
165
+ }
166
+
98
167
  export function isHttpUpstream(u: Upstream): u is HttpUpstream {
99
168
  return "url" in u;
100
169
  }
package/src/email.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Email notices over SMTP. Notify only, on purpose: emails never contain
3
+ * approve links, because mail security scanners open links automatically and
4
+ * would approve calls by themselves. Approving happens on the push channel.
5
+ *
6
+ * Emails name the tool, server and reason, never arguments or results.
7
+ */
8
+ import nodemailer from "nodemailer";
9
+ import type { ApprovalEvent, Notifier } from "./approvals.js";
10
+
11
+ export const EMAIL_EVENTS = ["pending", "blocked", "approved", "denied", "timeout", "undeliverable"] as const;
12
+ export type EmailEvent = (typeof EMAIL_EVENTS)[number];
13
+
14
+ export interface MailSender {
15
+ sendMail(mail: { from: string; to: string[]; subject: string; text: string }): Promise<unknown>;
16
+ }
17
+
18
+ export interface EmailOptions {
19
+ from: string;
20
+ to: string[];
21
+ /** Which events send an email. */
22
+ on: EmailEvent[];
23
+ /** SMTP settings, used unless `sender` is given. */
24
+ smtp?: { host: string; port: number; secure?: boolean; user?: string; pass?: string };
25
+ sender?: MailSender;
26
+ }
27
+
28
+ export class EmailNotifier implements Notifier {
29
+ readonly name = "email";
30
+ private readonly sender: MailSender;
31
+
32
+ constructor(private readonly opts: EmailOptions) {
33
+ if (opts.sender) {
34
+ this.sender = opts.sender;
35
+ } else if (opts.smtp) {
36
+ const { host, port, secure, user, pass } = opts.smtp;
37
+ this.sender = nodemailer.createTransport({
38
+ host,
39
+ port,
40
+ secure: secure ?? port === 465,
41
+ ...(user !== undefined ? { auth: { user, pass } } : {}),
42
+ });
43
+ } else {
44
+ throw new Error("EmailNotifier needs smtp settings or a sender");
45
+ }
46
+ }
47
+
48
+ async notify(event: ApprovalEvent): Promise<void> {
49
+ const kind: EmailEvent = event.kind === "resolved" ? event.result.outcome : event.kind;
50
+ if (!this.opts.on.includes(kind)) return;
51
+ const { subject, text } = this.compose(event);
52
+ await this.sender.sendMail({ from: this.opts.from, to: this.opts.to, subject, text });
53
+ }
54
+
55
+ private compose(event: ApprovalEvent): { subject: string; text: string } {
56
+ const footer = "\n\n-- \nSent by ADTL (adtl-gateway). This email never contains approve links; approve on your push channel.";
57
+ if (event.kind === "blocked") {
58
+ return {
59
+ subject: `ADTL blocked ${event.server}.${event.tool}`,
60
+ text: `ADTL blocked a call to ${event.server}.${event.tool}.\n\nReason: ${event.reason}\nRule: ${event.matched.join(", ") || "default"}\nSession: ${event.sessionId}${footer}`,
61
+ };
62
+ }
63
+ const r = event.request;
64
+ const subject = `${r.server}.${r.tool}`;
65
+ if (event.kind === "pending") {
66
+ return {
67
+ subject: `ADTL: approval needed for ${subject}`,
68
+ text:
69
+ `An agent wants to call ${subject} and ADTL needs your approval.\n\nReason: ${r.reason}\nRule: ${r.matched.join(", ") || "default"}\n` +
70
+ `Session: ${r.sessionId}\nExpires: ${r.expiresAt.toISOString()} (no answer means deny)${footer}`,
71
+ };
72
+ }
73
+ const res = event.result;
74
+ return {
75
+ subject: `ADTL: ${subject} ${res.outcome}`,
76
+ text: `The approval request for ${subject} was ${res.outcome}${res.channel ? ` via ${res.channel}` : ""} after ${Math.round(res.waitedMs / 1000)}s.\n\nReason it was asked: ${r.reason}\nSession: ${r.sessionId}${footer}`,
77
+ };
78
+ }
79
+ }