@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.
- package/README.md +48 -4
- package/dist/approvals.d.ts +97 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +151 -0
- package/dist/approvals.js.map +1 -0
- package/dist/audit.d.ts +10 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +352 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +69 -2
- package/dist/config.js.map +1 -1
- package/dist/email.d.ts +35 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +63 -0
- package/dist/email.js.map +1 -0
- package/dist/gateway.d.ts +12 -2
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +91 -11
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/invoker.d.ts +10 -1
- package/dist/invoker.d.ts.map +1 -1
- package/dist/invoker.js +15 -6
- package/dist/invoker.js.map +1 -1
- package/dist/ntfy.d.ts +44 -0
- package/dist/ntfy.d.ts.map +1 -0
- package/dist/ntfy.js +133 -0
- package/dist/ntfy.js.map +1 -0
- package/dist/startup.d.ts +3 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +39 -4
- package/dist/startup.js.map +1 -1
- package/package.json +4 -3
- package/src/approvals.ts +219 -0
- package/src/audit.ts +5 -0
- package/src/cli.ts +15 -4
- package/src/config.ts +71 -2
- package/src/email.ts +79 -0
- package/src/gateway.ts +103 -11
- package/src/index.ts +3 -0
- package/src/invoker.ts +17 -5
- package/src/ntfy.ts +154 -0
- package/src/startup.ts +36 -3
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ An MCP gateway for AI agents. It sits between your agent (Claude Code, Cursor, a
|
|
|
6
6
|
|
|
7
7
|
- **labels every tool result**: where it came from, whether it can be trusted, how sensitive it is (MCP's draft `trust-annotations` format)
|
|
8
8
|
- **checks every tool call** against what the session has already seen: allow, ask, or block
|
|
9
|
+
- **asks you on your phone** before risky calls, with Allow / Deny buttons (via [ntfy](https://ntfy.sh)), and can email you notices
|
|
9
10
|
- **writes an audit log** of every decision (no arguments or results are stored)
|
|
10
11
|
|
|
11
12
|
It stops attacks like the 2025 GitHub MCP exfiltration, where a public issue told an agent to post private repository contents in a comment.
|
|
@@ -63,7 +64,7 @@ Labels only get stricter as a session goes on. A session that has read anything
|
|
|
63
64
|
|
|
64
65
|
### Policy
|
|
65
66
|
|
|
66
|
-
Rules match on `session.integrity`, `session.confidentiality`, `tool.egress` and `tool` (`server.tool` globs such as `github.*`), and decide `allow`, `ask` or `block`. When several rules match, the most restrictive decision wins.
|
|
67
|
+
Rules match on `session.integrity`, `session.confidentiality`, `tool.egress` and `tool` (`server.tool` globs such as `github.*`), and decide `allow`, `ask` or `block`. When several rules match, the most restrictive decision wins. `ask` waits for a human when [approvals](#approvals) are configured, and blocks otherwise.
|
|
67
68
|
|
|
68
69
|
### Gateway
|
|
69
70
|
|
|
@@ -71,11 +72,54 @@ Rules match on `session.integrity`, `session.confidentiality`, `tool.egress` and
|
|
|
71
72
|
|---|---|
|
|
72
73
|
| `gateway.upstreams.<id>` | A stdio server (`command`, `args`, `env`, `cwd`) or an HTTP server (`url`, `headers`). The `<id>` must match the key under `servers:`. |
|
|
73
74
|
| `gateway.audit` | Path of a JSONL audit log, relative to the config file |
|
|
75
|
+
| `gateway.mode` | `active` (default) or `audit`. See [Trying a policy safely](#trying-a-policy-safely). |
|
|
76
|
+
| `gateway.approvals` | Ask a human about `ask` decisions. See [Approvals](#approvals). |
|
|
74
77
|
| `gateway.interceptorTimeoutMs` | Timeout per interceptor call (default 5000) |
|
|
75
78
|
| `gateway.interceptors` | Custom interceptor server command (default: the bundled `@adtl/interceptors`) |
|
|
76
79
|
|
|
77
80
|
`${VAR}` in commands, args, env, URLs and headers is read from the environment, so secrets stay out of the file.
|
|
78
81
|
|
|
82
|
+
### Approvals
|
|
83
|
+
|
|
84
|
+
With approvals configured, an `ask` decision holds the call and sends you a push notification with **Allow** and **Deny** buttons. Allow runs the call; Deny, or no answer before the timeout, stops it.
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
gateway:
|
|
88
|
+
approvals:
|
|
89
|
+
timeoutSeconds: 50 # no answer means deny (default 50)
|
|
90
|
+
ntfy:
|
|
91
|
+
topic: ${ADTL_NTFY_TOPIC} # a long random name; subscribe to it in the ntfy app
|
|
92
|
+
# server: https://ntfy.example.com # default https://ntfy.sh
|
|
93
|
+
# token: ${NTFY_TOKEN} # for access-controlled topics
|
|
94
|
+
email: # optional, notify only
|
|
95
|
+
smtp: { host: smtp.example.com, port: 465, user: "${SMTP_USER}", pass: "${SMTP_PASS}" }
|
|
96
|
+
from: adtl@example.com
|
|
97
|
+
to: you@example.com
|
|
98
|
+
on: [pending, blocked] # also: approved, denied, timeout, undeliverable
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Setup: install the [ntfy app](https://ntfy.sh), subscribe to your topic, and set `ADTL_NTFY_TOPIC` in the gateway's environment, for example in Claude Code's `.mcp.json` `env`.
|
|
102
|
+
|
|
103
|
+
How it works and what to know:
|
|
104
|
+
|
|
105
|
+
- **No open ports.** The buttons post your answer to a reply topic (`<topic>-reply`) on the same ntfy server, and the gateway listens there.
|
|
106
|
+
- **Answers are signed.** Each button carries a token that only answers that one call (it's bound to the call's hash), only once, and only until it expires. Forged, reused or late answers are ignored.
|
|
107
|
+
- **Keep the topic private.** Anyone who can read your topic can press your buttons. Use a long random topic name on ntfy.sh, or an access-controlled topic on your own ntfy server.
|
|
108
|
+
- **Nothing sensitive is sent.** Notifications and emails name the tool, the server and the reason, never the call's arguments or results.
|
|
109
|
+
- **Email never contains approve links,** because mail security scanners open links automatically and would approve calls by themselves.
|
|
110
|
+
- **Mind client timeouts.** Many MCP clients give up on a request after 60 seconds, so the default timeout is 50. The gateway sends progress notifications while it waits, which lets clients that reset their timeout on progress wait longer.
|
|
111
|
+
- **Every outcome is logged.** The audit log records `approval: { outcome, channel, waitedMs }` for each call that was asked.
|
|
112
|
+
|
|
113
|
+
### Trying a policy safely
|
|
114
|
+
|
|
115
|
+
Run in audit mode first to see what your policy would stop, without stopping anything:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
adtl-gateway --config adtl.yaml --audit
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
or set `gateway.mode: audit` in the file. In audit mode every call goes through and results are not changed. Labels are still tracked, so the decisions are the same as in active mode. Each line in the audit log gets `"mode": "audit"`, and a call the policy would have stopped is logged as `"outcome": "forwarded"` with `"decision": "ask"` or `"block"`, the reason and the matching rules. Once the log looks right, remove the flag.
|
|
122
|
+
|
|
79
123
|
## How it works
|
|
80
124
|
|
|
81
125
|
```
|
|
@@ -86,13 +130,13 @@ agent ──▶ adtl-gateway ──▶ upstream MCP servers
|
|
|
86
130
|
adtl.label labels the result after it returns
|
|
87
131
|
```
|
|
88
132
|
|
|
89
|
-
The gateway fails closed. If an interceptor errors or times out, the call is stopped. If a result can't be labeled, it is withheld. If the interceptors are missing, the gateway refuses to start.
|
|
133
|
+
The gateway fails closed. If an interceptor errors or times out, the call is stopped. If a result can't be labeled, it is withheld. If the interceptors are missing, the gateway refuses to start. (In audit mode such failures are logged as warnings instead.)
|
|
90
134
|
|
|
91
135
|
## Limits of this version
|
|
92
136
|
|
|
93
137
|
- Only tools are proxied. Resources and prompts are not passed through yet.
|
|
94
|
-
-
|
|
95
|
-
- Labels last for one gateway process, which is one agent session. They are not carried into files, memory or other agents yet; that's planned for v0.
|
|
138
|
+
- Approvals come from one push channel (ntfy). Other channels, approvals inside the MCP client, and approver roles are planned.
|
|
139
|
+
- Labels last for one gateway process, which is one agent session. They are not carried into files, memory or other agents yet; that's planned for v0.4.
|
|
96
140
|
- Labels are set by your config. Servers' own trust claims can only make labels stricter, unless you set `acceptServerClaims: true`.
|
|
97
141
|
|
|
98
142
|
## Links
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type ApprovalDecision = "allow" | "deny";
|
|
2
|
+
export type ApprovalOutcome = "approved" | "denied" | "timeout" | "undeliverable";
|
|
3
|
+
export interface ApprovalRequest {
|
|
4
|
+
id: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
server: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
callHash: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
matched: string[];
|
|
11
|
+
/** The session label that triggered the rule (no arguments or results). */
|
|
12
|
+
sessionLabel?: unknown;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
}
|
|
16
|
+
export interface ApprovalResult {
|
|
17
|
+
outcome: ApprovalOutcome;
|
|
18
|
+
/** Notifier that delivered the answer. */
|
|
19
|
+
channel?: string;
|
|
20
|
+
approver?: string;
|
|
21
|
+
waitedMs: number;
|
|
22
|
+
}
|
|
23
|
+
export type ApprovalEvent = {
|
|
24
|
+
kind: "pending";
|
|
25
|
+
request: ApprovalRequest;
|
|
26
|
+
tokens: Record<ApprovalDecision, string>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "resolved";
|
|
29
|
+
request: ApprovalRequest;
|
|
30
|
+
result: ApprovalResult;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "blocked";
|
|
33
|
+
sessionId: string;
|
|
34
|
+
server: string;
|
|
35
|
+
tool: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
matched: string[];
|
|
38
|
+
};
|
|
39
|
+
/** Called by a notifier with every answer it receives. Returns true if the answer was accepted. */
|
|
40
|
+
export type AnswerFn = (token: string, approver?: string) => boolean;
|
|
41
|
+
export interface Notifier {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** Deliver an event. Throwing marks the delivery as failed; it never stops the call by itself. */
|
|
44
|
+
notify(event: ApprovalEvent): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Notifiers that can carry answers: pass every answer for `request` to
|
|
47
|
+
* `answer` until `signal` aborts. Called before the pending event is sent.
|
|
48
|
+
*/
|
|
49
|
+
listen?(request: ApprovalRequest, answer: AnswerFn, signal: AbortSignal): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** Error message plus its cause, e.g. "fetch failed (ECONNRESET)". */
|
|
52
|
+
export declare function errorText(err: unknown): string;
|
|
53
|
+
export declare class ApprovalTokens {
|
|
54
|
+
private readonly secret;
|
|
55
|
+
constructor(secret?: Buffer);
|
|
56
|
+
sign(request: Pick<ApprovalRequest, "id" | "callHash" | "expiresAt">, decision: ApprovalDecision): string;
|
|
57
|
+
/** Parse a token without verifying it. */
|
|
58
|
+
static parse(token: string): {
|
|
59
|
+
id: string;
|
|
60
|
+
decision: ApprovalDecision;
|
|
61
|
+
exp: number;
|
|
62
|
+
sig: string;
|
|
63
|
+
} | undefined;
|
|
64
|
+
/** Verify a token against the pending request it claims to answer. */
|
|
65
|
+
verify(token: string, request: Pick<ApprovalRequest, "id" | "callHash">, now: Date): ApprovalDecision | undefined;
|
|
66
|
+
private mac;
|
|
67
|
+
}
|
|
68
|
+
export interface ApprovalBrokerOptions {
|
|
69
|
+
notifiers: Notifier[];
|
|
70
|
+
timeoutMs: number;
|
|
71
|
+
tokens?: ApprovalTokens;
|
|
72
|
+
now?: () => Date;
|
|
73
|
+
log?: {
|
|
74
|
+
warn(msg: string): void;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare class ApprovalBroker {
|
|
78
|
+
private readonly notifiers;
|
|
79
|
+
private readonly timeoutMs;
|
|
80
|
+
private readonly tokens;
|
|
81
|
+
private readonly now;
|
|
82
|
+
private readonly log;
|
|
83
|
+
private readonly pending;
|
|
84
|
+
constructor(opts: ApprovalBrokerOptions);
|
|
85
|
+
/** True if at least one notifier can carry answers. Without one, "ask" stays a block. */
|
|
86
|
+
get canApprove(): boolean;
|
|
87
|
+
/** Ask for approval and wait for the answer, the timeout, or a failure to reach anyone. */
|
|
88
|
+
request(input: Omit<ApprovalRequest, "id" | "createdAt" | "expiresAt">): Promise<ApprovalResult>;
|
|
89
|
+
/** Handle an answer token from a channel. Returns true if it settled a pending approval. */
|
|
90
|
+
answer(token: string, channel: string, approver?: string): boolean;
|
|
91
|
+
/** Tell notifiers a call was blocked. Fire and forget. */
|
|
92
|
+
blocked(event: Omit<Extract<ApprovalEvent, {
|
|
93
|
+
kind: "blocked";
|
|
94
|
+
}>, "kind">): void;
|
|
95
|
+
private broadcast;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=approvals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
const TOKEN_PREFIX = "adtl1";
|
|
16
|
+
/** Error message plus its cause, e.g. "fetch failed (ECONNRESET)". */
|
|
17
|
+
export function errorText(err) {
|
|
18
|
+
if (!(err instanceof Error))
|
|
19
|
+
return String(err);
|
|
20
|
+
const cause = err.cause;
|
|
21
|
+
const detail = cause?.code ?? cause?.message;
|
|
22
|
+
return detail && !err.message.includes(detail) ? `${err.message} (${detail})` : err.message;
|
|
23
|
+
}
|
|
24
|
+
export class ApprovalTokens {
|
|
25
|
+
secret;
|
|
26
|
+
constructor(secret = randomBytes(32)) {
|
|
27
|
+
this.secret = secret;
|
|
28
|
+
}
|
|
29
|
+
sign(request, decision) {
|
|
30
|
+
const exp = Math.floor(request.expiresAt.getTime() / 1000);
|
|
31
|
+
return `${TOKEN_PREFIX}.${request.id}.${decision}.${exp}.${this.mac(request.id, request.callHash, decision, exp)}`;
|
|
32
|
+
}
|
|
33
|
+
/** Parse a token without verifying it. */
|
|
34
|
+
static parse(token) {
|
|
35
|
+
const parts = token.trim().split(".");
|
|
36
|
+
if (parts.length !== 5 || parts[0] !== TOKEN_PREFIX)
|
|
37
|
+
return undefined;
|
|
38
|
+
const [, id, decision, exp, sig] = parts;
|
|
39
|
+
if (decision !== "allow" && decision !== "deny")
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!/^\d+$/.test(exp))
|
|
42
|
+
return undefined;
|
|
43
|
+
return { id, decision, exp: Number(exp), sig };
|
|
44
|
+
}
|
|
45
|
+
/** Verify a token against the pending request it claims to answer. */
|
|
46
|
+
verify(token, request, now) {
|
|
47
|
+
const t = ApprovalTokens.parse(token);
|
|
48
|
+
if (!t || t.id !== request.id)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (now.getTime() > t.exp * 1000)
|
|
51
|
+
return undefined;
|
|
52
|
+
const expected = Buffer.from(this.mac(t.id, request.callHash, t.decision, t.exp));
|
|
53
|
+
const given = Buffer.from(t.sig);
|
|
54
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
55
|
+
return undefined;
|
|
56
|
+
return t.decision;
|
|
57
|
+
}
|
|
58
|
+
mac(id, callHash, decision, exp) {
|
|
59
|
+
return createHmac("sha256", this.secret).update(`${id}\n${callHash}\n${decision}\n${exp}`).digest("base64url");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class ApprovalBroker {
|
|
63
|
+
notifiers;
|
|
64
|
+
timeoutMs;
|
|
65
|
+
tokens;
|
|
66
|
+
now;
|
|
67
|
+
log;
|
|
68
|
+
pending = new Map();
|
|
69
|
+
constructor(opts) {
|
|
70
|
+
this.notifiers = opts.notifiers;
|
|
71
|
+
this.timeoutMs = opts.timeoutMs;
|
|
72
|
+
this.tokens = opts.tokens ?? new ApprovalTokens();
|
|
73
|
+
this.now = opts.now ?? (() => new Date());
|
|
74
|
+
this.log = opts.log ?? { warn: () => { } };
|
|
75
|
+
}
|
|
76
|
+
/** True if at least one notifier can carry answers. Without one, "ask" stays a block. */
|
|
77
|
+
get canApprove() {
|
|
78
|
+
return this.notifiers.some((n) => n.listen);
|
|
79
|
+
}
|
|
80
|
+
/** Ask for approval and wait for the answer, the timeout, or a failure to reach anyone. */
|
|
81
|
+
async request(input) {
|
|
82
|
+
const createdAt = this.now();
|
|
83
|
+
const request = {
|
|
84
|
+
...input,
|
|
85
|
+
id: randomUUID(),
|
|
86
|
+
createdAt,
|
|
87
|
+
expiresAt: new Date(createdAt.getTime() + this.timeoutMs),
|
|
88
|
+
};
|
|
89
|
+
const started = performance.now();
|
|
90
|
+
const waited = () => Math.round(performance.now() - started);
|
|
91
|
+
const abort = new AbortController();
|
|
92
|
+
let finish;
|
|
93
|
+
const result = new Promise((resolve) => {
|
|
94
|
+
const timer = setTimeout(() => finish({ outcome: "timeout", waitedMs: waited() }), this.timeoutMs);
|
|
95
|
+
finish = (r) => {
|
|
96
|
+
if (!this.pending.delete(request.id))
|
|
97
|
+
return; // already settled
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
abort.abort();
|
|
100
|
+
resolve(r);
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
this.pending.set(request.id, {
|
|
104
|
+
request,
|
|
105
|
+
settle: (decision, channel, approver) => finish({ outcome: decision === "allow" ? "approved" : "denied", channel, ...(approver ? { approver } : {}), waitedMs: waited() }),
|
|
106
|
+
});
|
|
107
|
+
// Start listening before anyone can answer, then send the request everywhere.
|
|
108
|
+
const listeners = this.notifiers.filter((n) => n.listen);
|
|
109
|
+
for (const n of listeners) {
|
|
110
|
+
n.listen(request, (token, approver) => this.answer(token, n.name, approver), abort.signal).catch((err) => this.log.warn(`${n.name}: stopped listening for approvals: ${errorText(err)}`));
|
|
111
|
+
}
|
|
112
|
+
const tokens = { allow: this.tokens.sign(request, "allow"), deny: this.tokens.sign(request, "deny") };
|
|
113
|
+
const sent = this.notifiers.map((n) => n.notify({ kind: "pending", request, tokens }).then(() => true, (err) => {
|
|
114
|
+
this.log.warn(`${n.name}: could not send approval request: ${errorText(err)}`);
|
|
115
|
+
return false;
|
|
116
|
+
}));
|
|
117
|
+
// Nobody who could answer was reached: deny now rather than wait out the timeout.
|
|
118
|
+
// Notify-only channels (email) are not waited for.
|
|
119
|
+
void Promise.all(sent.filter((_, i) => this.notifiers[i].listen)).then((ok) => {
|
|
120
|
+
if (!ok.includes(true))
|
|
121
|
+
finish({ outcome: "undeliverable", waitedMs: waited() });
|
|
122
|
+
});
|
|
123
|
+
const r = await result;
|
|
124
|
+
this.broadcast({ kind: "resolved", request, result: r });
|
|
125
|
+
return r;
|
|
126
|
+
}
|
|
127
|
+
/** Handle an answer token from a channel. Returns true if it settled a pending approval. */
|
|
128
|
+
answer(token, channel, approver) {
|
|
129
|
+
const parsed = ApprovalTokens.parse(token);
|
|
130
|
+
const p = parsed && this.pending.get(parsed.id);
|
|
131
|
+
if (!p)
|
|
132
|
+
return false; // not ours, already answered, or garbage
|
|
133
|
+
const decision = this.tokens.verify(token, p.request, this.now());
|
|
134
|
+
if (!decision) {
|
|
135
|
+
this.log.warn(`${channel}: ignored an approval answer with a bad or expired signature`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
p.settle(decision, channel, approver);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/** Tell notifiers a call was blocked. Fire and forget. */
|
|
142
|
+
blocked(event) {
|
|
143
|
+
this.broadcast({ kind: "blocked", ...event });
|
|
144
|
+
}
|
|
145
|
+
broadcast(event) {
|
|
146
|
+
for (const n of this.notifiers) {
|
|
147
|
+
n.notify(event).catch((err) => this.log.warn(`${n.name}: could not send ${event.kind} notice: ${errorText(err)}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=approvals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|
package/dist/audit.d.ts
CHANGED
|
@@ -6,7 +6,17 @@ export interface AuditEntry {
|
|
|
6
6
|
tool: string;
|
|
7
7
|
/** Final outcome of the call. */
|
|
8
8
|
outcome: "forwarded" | "blocked" | "upstream_error";
|
|
9
|
+
/** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
|
|
9
10
|
decision?: string;
|
|
11
|
+
/** Present when the gateway ran in audit mode: nothing was stopped or changed. */
|
|
12
|
+
mode?: "audit";
|
|
13
|
+
/** Present when a human was asked: outcome, channel, approver (if known) and wait time. */
|
|
14
|
+
approval?: {
|
|
15
|
+
outcome: string;
|
|
16
|
+
channel?: string;
|
|
17
|
+
approver?: string;
|
|
18
|
+
waitedMs: number;
|
|
19
|
+
};
|
|
10
20
|
reason?: string;
|
|
11
21
|
matched?: unknown;
|
|
12
22
|
callHash?: unknown;
|
package/dist/audit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,qBAAa,aAAc,YAAW,SAAS;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;IAIjC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B;AAED,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAM;IACpC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B"}
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACpD,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,qBAAa,aAAc,YAAW,SAAS;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;IAIjC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B;AAED,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAM;IACpC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAG/B"}
|
package/dist/audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,MAAM,OAAO,aAAa;IACH;IAArB,YAAqB,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QAC/B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,KAAiB;QACrB,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IAChB,OAAO,GAAiB,EAAE,CAAC;IACpC,KAAK,CAAC,KAAiB;QACrB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;CACF"}
|
package/dist/cli.js
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.
|
|
@@ -12,13 +12,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
12
|
import { EnvExpansionError } from "./config.js";
|
|
13
13
|
import { GATEWAY_VERSION } from "./gateway.js";
|
|
14
14
|
import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
|
|
15
|
-
const USAGE = `Usage: adtl-gateway --config <adtl.yaml>
|
|
15
|
+
const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
|
|
16
16
|
|
|
17
17
|
Runs an MCP server over stdio that forwards tool calls to the upstream
|
|
18
18
|
servers in the config, checking and labeling each call with ADTL.
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
-c, --config <path> ADTL config file with a gateway: section. Required.
|
|
22
|
+
--audit Log what would be stopped, but stop nothing
|
|
23
|
+
(same as gateway.mode: audit).
|
|
22
24
|
-v, --version Print the version and exit.
|
|
23
25
|
-h, --help Show this help.`;
|
|
24
26
|
function parseArgs(argv) {
|
|
@@ -33,6 +35,8 @@ function parseArgs(argv) {
|
|
|
33
35
|
out.config = argv[++i];
|
|
34
36
|
else if (a?.startsWith("--config="))
|
|
35
37
|
out.config = a.slice("--config=".length);
|
|
38
|
+
else if (a === "--audit")
|
|
39
|
+
out.audit = true;
|
|
36
40
|
else
|
|
37
41
|
throw new GatewayConfigError(`Unknown argument: ${a}\n\n${USAGE}`);
|
|
38
42
|
}
|
|
@@ -48,6 +52,8 @@ async function main() {
|
|
|
48
52
|
if (!args.config)
|
|
49
53
|
throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
|
|
50
54
|
const loaded = await loadGatewayConfig(args.config);
|
|
55
|
+
if (args.audit)
|
|
56
|
+
loaded.gateway.mode = "audit";
|
|
51
57
|
const started = await startGateway(loaded);
|
|
52
58
|
const server = started.gateway.createServer();
|
|
53
59
|
let closing = false;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEnF,MAAM,KAAK,GAAG
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|