@adtl/gateway 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +57 -5
  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 +12 -2
  7. package/dist/audit.d.ts.map +1 -1
  8. package/dist/audit.js +1 -1
  9. package/dist/audit.js.map +1 -1
  10. package/dist/cli.js +46 -1
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +346 -4
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +63 -2
  15. package/dist/config.js.map +1 -1
  16. package/dist/email.d.ts +35 -0
  17. package/dist/email.d.ts.map +1 -0
  18. package/dist/email.js +63 -0
  19. package/dist/email.js.map +1 -0
  20. package/dist/gateway.d.ts +33 -3
  21. package/dist/gateway.d.ts.map +1 -1
  22. package/dist/gateway.js +263 -23
  23. package/dist/gateway.js.map +1 -1
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/init/index.d.ts +4 -0
  29. package/dist/init/index.d.ts.map +1 -0
  30. package/dist/init/index.js +4 -0
  31. package/dist/init/index.js.map +1 -0
  32. package/dist/init/presets.d.ts +30 -0
  33. package/dist/init/presets.d.ts.map +1 -0
  34. package/dist/init/presets.js +34 -0
  35. package/dist/init/presets.js.map +1 -0
  36. package/dist/init/run.d.ts +18 -0
  37. package/dist/init/run.d.ts.map +1 -0
  38. package/dist/init/run.js +60 -0
  39. package/dist/init/run.js.map +1 -0
  40. package/dist/init/suggest.d.ts +50 -0
  41. package/dist/init/suggest.d.ts.map +1 -0
  42. package/dist/init/suggest.js +82 -0
  43. package/dist/init/suggest.js.map +1 -0
  44. package/dist/invoker.d.ts.map +1 -1
  45. package/dist/invoker.js +4 -2
  46. package/dist/invoker.js.map +1 -1
  47. package/dist/ntfy.d.ts +44 -0
  48. package/dist/ntfy.d.ts.map +1 -0
  49. package/dist/ntfy.js +133 -0
  50. package/dist/ntfy.js.map +1 -0
  51. package/dist/startup.d.ts +6 -1
  52. package/dist/startup.d.ts.map +1 -1
  53. package/dist/startup.js +37 -5
  54. package/dist/startup.js.map +1 -1
  55. package/package.json +4 -3
  56. package/src/approvals.ts +219 -0
  57. package/src/audit.ts +8 -3
  58. package/src/cli.ts +41 -1
  59. package/src/config.ts +65 -2
  60. package/src/email.ts +79 -0
  61. package/src/gateway.ts +324 -22
  62. package/src/index.ts +4 -0
  63. package/src/init/index.ts +3 -0
  64. package/src/init/presets.ts +64 -0
  65. package/src/init/run.ts +79 -0
  66. package/src/init/suggest.ts +127 -0
  67. package/src/invoker.ts +3 -2
  68. package/src/ntfy.ts +154 -0
  69. package/src/startup.ts +35 -4
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);
@@ -68,7 +71,7 @@ function stdioTransport(u, baseDir) {
68
71
  stderr: "inherit",
69
72
  });
70
73
  }
71
- async function connect(id, u, baseDir) {
74
+ export async function connectUpstream(id, u, baseDir) {
72
75
  const client = new Client({ name: `adtl-gateway→${id}`, version: GATEWAY_VERSION });
73
76
  const transport = isHttpUpstream(u)
74
77
  ? new StreamableHTTPClientTransport(new URL(u.url), { requestInit: { headers: u.headers } })
@@ -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);
@@ -87,7 +112,7 @@ export async function startGateway(loaded, opts = {}) {
87
112
  const icfg = loaded.gateway.interceptors
88
113
  ? expandUpstream("interceptors", loaded.gateway.interceptors, env)
89
114
  : { command: process.execPath, args: [bundledInterceptorsCli(), "--config", loaded.path], env: {} };
90
- const interceptors = await connect("interceptors", icfg, baseDir);
115
+ const interceptors = await connectUpstream("interceptors", icfg, baseDir);
91
116
  clients.push(interceptors);
92
117
  if (!interceptors.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
93
118
  throw new GatewayConfigError(`The interceptor server does not declare the ${INTERCEPTORS_EXTENSION} extension`);
@@ -107,7 +132,7 @@ export async function startGateway(loaded, opts = {}) {
107
132
  // 2. Upstream servers. One failing upstream does not stop the others.
108
133
  const upstreams = new Map();
109
134
  const ids = Object.keys(loaded.gateway.upstreams);
110
- const settled = await Promise.allSettled(ids.map((id) => connect(id, expandUpstream(id, loaded.gateway.upstreams[id], env), baseDir)));
135
+ const settled = await Promise.allSettled(ids.map((id) => connectUpstream(id, expandUpstream(id, loaded.gateway.upstreams[id], env), baseDir)));
111
136
  settled.forEach((s, i) => {
112
137
  const id = ids[i];
113
138
  if (s.status === "fulfilled") {
@@ -125,7 +150,14 @@ export async function startGateway(loaded, opts = {}) {
125
150
  throw new GatewayConfigError("No upstream server could be connected");
126
151
  log.info(`upstreams: ${[...upstreams.keys()].join(", ")}`);
127
152
  const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
128
- const gateway = new AdtlGateway({ upstreams, invoker, ...(auditLog ? { audit: auditLog } : {}), logger: log });
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
+ });
129
161
  return { gateway, close: closeAll };
130
162
  }
131
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.1",
3
+ "version": "0.3.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.1",
50
- "@adtl/labels": "0.1.1",
49
+ "@adtl/interceptors": "0.3.0",
50
+ "@adtl/labels": "0.3.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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Append-only JSONL audit log. One line per tool call.
2
+ * Append-only JSONL audit log. One line per tool call, resource read or prompt get.
3
3
  *
4
4
  * Arguments and results are never written; the call is identified by its
5
5
  * hash, so the log can be shared without leaking the data it describes.
@@ -10,15 +10,20 @@ import { dirname } from "node:path";
10
10
  export interface AuditEntry {
11
11
  ts: string;
12
12
  sessionId: string;
13
- event: "tools/call";
13
+ event: "tools/call" | "resources/read" | "prompts/get";
14
14
  server: string;
15
- tool: string;
15
+ /** Exactly one of tool, resource or prompt is set, matching `event`. */
16
+ tool?: string;
17
+ resource?: string;
18
+ prompt?: string;
16
19
  /** Final outcome of the call. */
17
20
  outcome: "forwarded" | "blocked" | "upstream_error";
18
21
  /** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
19
22
  decision?: string;
20
23
  /** Present when the gateway ran in audit mode: nothing was stopped or changed. */
21
24
  mode?: "audit";
25
+ /** Present when a human was asked: outcome, channel, approver (if known) and wait time. */
26
+ approval?: { outcome: string; channel?: string; approver?: string; waitedMs: number };
22
27
  reason?: string;
23
28
  matched?: unknown;
24
29
  callHash?: unknown;
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * servers through ADTL's interceptors.
5
5
  *
6
6
  * adtl-gateway --config adtl.yaml [--audit]
7
+ * adtl-gateway init --config adtl.yaml [--write]
7
8
  *
8
9
  * Point your MCP client (e.g. Claude Code's .mcp.json) at this command.
9
10
  * Logs go to stderr; stdout carries MCP JSON-RPC only.
@@ -11,9 +12,11 @@
11
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
13
  import { EnvExpansionError } from "./config.js";
13
14
  import { GATEWAY_VERSION } from "./gateway.js";
15
+ import { runInit, type InitResult } from "./init/run.js";
14
16
  import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
15
17
 
16
18
  const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
19
+ adtl-gateway init --config <adtl.yaml> [--write]
17
20
 
18
21
  Runs an MCP server over stdio that forwards tool calls to the upstream
19
22
  servers in the config, checking and labeling each call with ADTL.
@@ -23,9 +26,18 @@ Options:
23
26
  --audit Log what would be stopped, but stop nothing
24
27
  (same as gateway.mode: audit).
25
28
  -v, --version Print the version and exit.
29
+
30
+ init:
31
+ Connects to every upstream, lists its tools, and drafts a labeling rule
32
+ for each tool the config doesn't cover yet (presets for GitHub,
33
+ filesystem and fetch; MCP tool hints otherwise). Existing entries are
34
+ never changed. Prints the updated config; review it before using it.
35
+ --write Update the config file in place (original kept as .bak).
26
36
  -h, --help Show this help.`;
27
37
 
28
38
  interface Args {
39
+ init?: boolean;
40
+ write?: boolean;
29
41
  config?: string;
30
42
  audit?: boolean;
31
43
  help?: boolean;
@@ -36,11 +48,13 @@ function parseArgs(argv: string[]): Args {
36
48
  const out: Args = {};
37
49
  for (let i = 0; i < argv.length; i++) {
38
50
  const a = argv[i];
39
- if (a === "-h" || a === "--help") out.help = true;
51
+ if (i === 0 && a === "init") out.init = true;
52
+ else if (a === "-h" || a === "--help") out.help = true;
40
53
  else if (a === "-v" || a === "--version") out.version = true;
41
54
  else if (a === "-c" || a === "--config") out.config = argv[++i];
42
55
  else if (a?.startsWith("--config=")) out.config = a.slice("--config=".length);
43
56
  else if (a === "--audit") out.audit = true;
57
+ else if (a === "--write" && out.init) out.write = true;
44
58
  else throw new GatewayConfigError(`Unknown argument: ${a}\n\n${USAGE}`);
45
59
  }
46
60
  return out;
@@ -52,6 +66,7 @@ async function main(): Promise<void> {
52
66
  if (args.help) return void console.log(USAGE);
53
67
  if (args.version) return void console.log(GATEWAY_VERSION);
54
68
  if (!args.config) throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
69
+ if (args.init) return init(args.config, args.write === true);
55
70
 
56
71
  const loaded = await loadGatewayConfig(args.config);
57
72
  if (args.audit) loaded.gateway.mode = "audit";
@@ -77,6 +92,31 @@ async function main(): Promise<void> {
77
92
  console.error(`[adtl-gateway] ${GATEWAY_VERSION} ready (session ${started.gateway.sessionId})`);
78
93
  }
79
94
 
95
+ function summarize(r: InitResult, path: string, write: boolean): string {
96
+ const lines = r.reports.map((s) => {
97
+ const parts = [`${s.added.length} tools added`];
98
+ if (s.kept.length) parts.push(`${s.kept.length} kept`);
99
+ if (s.addedDefaults) parts.push("defaults added");
100
+ if (s.stale.length) parts.push(`no longer offered: ${s.stale.join(", ")}`);
101
+ return ` ${s.id} (${s.preset ? `preset ${s.preset}` : "from tool hints, review"}): ${parts.join("; ")}`;
102
+ });
103
+ for (const f of r.failed) lines.push(` ${f.id}: could not connect, skipped (${f.error})`);
104
+ const changed = r.reports.some((s) => s.added.length || s.addedDefaults);
105
+ const tail = !changed
106
+ ? "Nothing to add."
107
+ : write
108
+ ? `Updated ${path} (original saved as ${path}.bak). Review every line marked "review".`
109
+ : "Nothing written. Review the config above, then re-run with --write.";
110
+ return `${lines.join("\n")}\n${tail}`;
111
+ }
112
+
113
+ async function init(path: string, write: boolean): Promise<void> {
114
+ const result = await runInit({ path, write });
115
+ if (!write) process.stdout.write(result.text);
116
+ console.error(`[adtl-gateway] init\n${summarize(result, path, write)}`);
117
+ if (result.failed.length && !result.reports.length) process.exitCode = 1;
118
+ }
119
+
80
120
  main().catch((err: unknown) => {
81
121
  const known = err instanceof GatewayConfigError || err instanceof EnvExpansionError;
82
122
  console.error(known ? `[adtl-gateway] ${(err as Error).message}` : err);
package/src/config.ts CHANGED
@@ -12,11 +12,19 @@
12
12
  * docs:
13
13
  * url: https://example.com/mcp
14
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
15
22
  *
16
- * `${VAR}` in command, args, env, cwd, url and headers is replaced from the
17
- * 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.
18
25
  */
19
26
  import { z } from "zod";
27
+ import { EMAIL_EVENTS } from "./email.js";
20
28
 
21
29
  const UPSTREAM_ID = /^[A-Za-z0-9-]+$/;
22
30
 
@@ -41,6 +49,48 @@ export type StdioUpstream = z.infer<typeof StdioUpstreamSchema>;
41
49
  export type HttpUpstream = z.infer<typeof HttpUpstreamSchema>;
42
50
  export type Upstream = z.infer<typeof UpstreamSchema>;
43
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
+
44
94
  export const GatewayConfigSchema = z
45
95
  .object({
46
96
  upstreams: z
@@ -58,6 +108,8 @@ export const GatewayConfigSchema = z
58
108
  * logs what would have been stopped: for trying a policy out safely.
59
109
  */
60
110
  mode: z.enum(["active", "audit"]).default("active"),
111
+ /** Human approvals for "ask" decisions. Without an answering channel, "ask" blocks. */
112
+ approvals: ApprovalsConfigSchema.optional(),
61
113
  /** Timeout for each interceptor invocation, in milliseconds. */
62
114
  interceptorTimeoutMs: z.number().int().positive().default(5000),
63
115
  })
@@ -101,6 +153,17 @@ export function expandUpstream<T extends Upstream>(id: string, u: T, env: NodeJS
101
153
  };
102
154
  }
103
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
+
104
167
  export function isHttpUpstream(u: Upstream): u is HttpUpstream {
105
168
  return "url" in u;
106
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
+ }