@adtl/gateway 0.3.0 → 0.3.1
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 +42 -1
- package/dist/audit.d.ts +4 -2
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +36 -1
- package/dist/cli.js.map +1 -1
- package/dist/gateway.d.ts +1 -1
- package/dist/gateway.js +1 -1
- package/dist/hooks/claudeCode.d.ts +43 -0
- package/dist/hooks/claudeCode.d.ts.map +1 -0
- package/dist/hooks/claudeCode.js +148 -0
- package/dist/hooks/claudeCode.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/preset.d.ts +16 -0
- package/dist/hooks/preset.d.ts.map +1 -0
- package/dist/hooks/preset.js +32 -0
- package/dist/hooks/preset.js.map +1 -0
- package/dist/hooks/run.d.ts +9 -0
- package/dist/hooks/run.d.ts.map +1 -0
- package/dist/hooks/run.js +44 -0
- package/dist/hooks/run.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/startup.d.ts +10 -1
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +22 -3
- package/dist/startup.js.map +1 -1
- package/package.json +3 -3
- package/src/audit.ts +4 -2
- package/src/cli.ts +34 -1
- package/src/gateway.ts +1 -1
- package/src/hooks/claudeCode.ts +194 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/preset.ts +47 -0
- package/src/hooks/run.ts +49 -0
- package/src/index.ts +1 -0
- package/src/startup.ts +22 -4
package/dist/startup.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type AdtlConfig } from "@adtl/labels";
|
|
3
3
|
import { ApprovalBroker } from "./approvals.js";
|
|
4
4
|
import { type GatewayConfig, type Upstream } from "./config.js";
|
|
5
5
|
import { AdtlGateway, type GatewayLogger } from "./gateway.js";
|
|
@@ -17,6 +17,15 @@ export declare function bundledInterceptorsCli(): string;
|
|
|
17
17
|
export declare function connectUpstream(id: string, u: Upstream, baseDir: string): Promise<Client>;
|
|
18
18
|
/** Build the approval broker from `gateway.approvals`, or undefined if not configured. */
|
|
19
19
|
export declare function createApprovals(cfg: GatewayConfig, env: NodeJS.ProcessEnv, log: GatewayLogger): ApprovalBroker | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
22
|
+
* servers, the gateway uses that id and keeps session labels in files, so the
|
|
23
|
+
* Claude Code hooks (`adtl-gateway hook`) add to the same session label.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sharedSession(env: NodeJS.ProcessEnv): {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
dir: string;
|
|
28
|
+
} | undefined;
|
|
20
29
|
export interface StartedGateway {
|
|
21
30
|
gateway: AdtlGateway;
|
|
22
31
|
close(): Promise<void>;
|
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,KAAK,
|
|
1
|
+
{"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAGnE,OAAO,EAAE,cAAc,EAAiB,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAKL,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,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;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,CAiB1H;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,CAwEzB"}
|
package/dist/startup.js
CHANGED
|
@@ -9,6 +9,7 @@ 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
13
|
import { parse as parseYaml } from "yaml";
|
|
13
14
|
import { ZodError } from "zod";
|
|
14
15
|
import { ApprovalBroker } from "./approvals.js";
|
|
@@ -101,6 +102,15 @@ export function createApprovals(cfg, env, log) {
|
|
|
101
102
|
log.info(`approvals: ${notifiers.map((n) => n.name).join(", ")} (timeout ${a.timeoutSeconds}s)`);
|
|
102
103
|
return new ApprovalBroker({ notifiers, timeoutMs: a.timeoutSeconds * 1000, log });
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
107
|
+
* servers, the gateway uses that id and keeps session labels in files, so the
|
|
108
|
+
* Claude Code hooks (`adtl-gateway hook`) add to the same session label.
|
|
109
|
+
*/
|
|
110
|
+
export function sharedSession(env) {
|
|
111
|
+
const sessionId = env.CLAUDE_CODE_SESSION_ID?.trim();
|
|
112
|
+
return sessionId ? { sessionId, dir: defaultSessionsDir(env) } : undefined;
|
|
113
|
+
}
|
|
104
114
|
export async function startGateway(loaded, opts = {}) {
|
|
105
115
|
const env = opts.env ?? process.env;
|
|
106
116
|
const baseDir = dirname(loaded.path);
|
|
@@ -109,9 +119,15 @@ export async function startGateway(loaded, opts = {}) {
|
|
|
109
119
|
const closeAll = () => Promise.allSettled(clients.map((c) => c.close())).then(() => undefined);
|
|
110
120
|
try {
|
|
111
121
|
// 1. Interceptor server (out of process, per the Interceptors WG).
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
const shared = sharedSession(env);
|
|
123
|
+
const custom = loaded.gateway.interceptors && expandUpstream("interceptors", loaded.gateway.interceptors, env);
|
|
124
|
+
const icfg = custom
|
|
125
|
+
? { ...custom, env: { ...custom.env, ...(shared ? { ADTL_SESSIONS_DIR: shared.dir } : {}) } }
|
|
126
|
+
: {
|
|
127
|
+
command: process.execPath,
|
|
128
|
+
args: [bundledInterceptorsCli(), "--config", loaded.path, ...(shared ? ["--sessions-dir", shared.dir] : [])],
|
|
129
|
+
env: {},
|
|
130
|
+
};
|
|
115
131
|
const interceptors = await connectUpstream("interceptors", icfg, baseDir);
|
|
116
132
|
clients.push(interceptors);
|
|
117
133
|
if (!interceptors.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
|
|
@@ -127,6 +143,8 @@ export async function startGateway(loaded, opts = {}) {
|
|
|
127
143
|
const audit = loaded.gateway.mode === "audit";
|
|
128
144
|
const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined);
|
|
129
145
|
log.info(`interceptors: ${names.join(", ")}`);
|
|
146
|
+
if (shared)
|
|
147
|
+
log.info(`sharing session labels with Claude Code hooks (${shared.dir})`);
|
|
130
148
|
if (audit)
|
|
131
149
|
log.warn("audit mode: calls are logged but never stopped or changed");
|
|
132
150
|
// 2. Upstream servers. One failing upstream does not stop the others.
|
|
@@ -152,6 +170,7 @@ export async function startGateway(loaded, opts = {}) {
|
|
|
152
170
|
const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
|
|
153
171
|
const approvals = createApprovals(loaded.gateway, env, log);
|
|
154
172
|
const gateway = new AdtlGateway({
|
|
173
|
+
...(shared ? { sessionId: shared.sessionId } : {}),
|
|
155
174
|
upstreams,
|
|
156
175
|
invoker,
|
|
157
176
|
...(auditLog ? { audit: auditLog } : {}),
|
package/dist/startup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup.js","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AACxG,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;
|
|
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.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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,8 +46,8 @@
|
|
|
46
46
|
"url": "https://github.com/probita-ai/adtl/issues"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adtl/interceptors": "0.3.
|
|
50
|
-
"@adtl/labels": "0.3.
|
|
49
|
+
"@adtl/interceptors": "0.3.1",
|
|
50
|
+
"@adtl/labels": "0.3.1",
|
|
51
51
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
52
52
|
"nodemailer": "10.0.12",
|
|
53
53
|
"yaml": "2.9.1",
|
package/src/audit.ts
CHANGED
|
@@ -16,8 +16,10 @@ export interface AuditEntry {
|
|
|
16
16
|
tool?: string;
|
|
17
17
|
resource?: string;
|
|
18
18
|
prompt?: string;
|
|
19
|
-
/** Final outcome of the call. */
|
|
20
|
-
outcome: "forwarded" | "blocked" | "upstream_error";
|
|
19
|
+
/** Final outcome of the call. "asked": handed to Claude Code's permission prompt by the hook. */
|
|
20
|
+
outcome: "forwarded" | "blocked" | "asked" | "upstream_error";
|
|
21
|
+
/** Present when the entry was written by the Claude Code hook rather than the gateway. */
|
|
22
|
+
via?: "claude-code-hook";
|
|
21
23
|
/** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
|
|
22
24
|
decision?: string;
|
|
23
25
|
/** Present when the gateway ran in audit mode: nothing was stopped or changed. */
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* adtl-gateway --config adtl.yaml [--audit]
|
|
7
7
|
* adtl-gateway init --config adtl.yaml [--write]
|
|
8
|
+
* adtl-gateway hook --config adtl.yaml [--gateway-server adtl]
|
|
8
9
|
*
|
|
9
10
|
* Point your MCP client (e.g. Claude Code's .mcp.json) at this command.
|
|
10
11
|
* Logs go to stderr; stdout carries MCP JSON-RPC only.
|
|
@@ -12,11 +13,13 @@
|
|
|
12
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
14
|
import { EnvExpansionError } from "./config.js";
|
|
14
15
|
import { GATEWAY_VERSION } from "./gateway.js";
|
|
16
|
+
import { runHook } from "./hooks/run.js";
|
|
15
17
|
import { runInit, type InitResult } from "./init/run.js";
|
|
16
18
|
import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
|
|
17
19
|
|
|
18
20
|
const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
|
|
19
21
|
adtl-gateway init --config <adtl.yaml> [--write]
|
|
22
|
+
adtl-gateway hook --config <adtl.yaml> [--gateway-server <name>]
|
|
20
23
|
|
|
21
24
|
Runs an MCP server over stdio that forwards tool calls to the upstream
|
|
22
25
|
servers in the config, checking and labeling each call with ADTL.
|
|
@@ -33,10 +36,23 @@ init:
|
|
|
33
36
|
filesystem and fetch; MCP tool hints otherwise). Existing entries are
|
|
34
37
|
never changed. Prints the updated config; review it before using it.
|
|
35
38
|
--write Update the config file in place (original kept as .bak).
|
|
39
|
+
|
|
40
|
+
hook:
|
|
41
|
+
The Claude Code hook for PreToolUse, PostToolUse and PostToolUseFailure.
|
|
42
|
+
Labels built-in tools (WebFetch, Read, Bash, ...) and MCP servers outside
|
|
43
|
+
the gateway, and runs the same policy on them, sharing the session label
|
|
44
|
+
with the gateway. Reads the hook event from stdin.
|
|
45
|
+
--gateway-server <name>
|
|
46
|
+
The gateway's name in Claude Code's MCP config; its
|
|
47
|
+
tools are checked by the gateway and skipped here.
|
|
48
|
+
Default: adtl.
|
|
49
|
+
|
|
36
50
|
-h, --help Show this help.`;
|
|
37
51
|
|
|
38
52
|
interface Args {
|
|
39
53
|
init?: boolean;
|
|
54
|
+
hook?: boolean;
|
|
55
|
+
gatewayServer?: string;
|
|
40
56
|
write?: boolean;
|
|
41
57
|
config?: string;
|
|
42
58
|
audit?: boolean;
|
|
@@ -49,6 +65,8 @@ function parseArgs(argv: string[]): Args {
|
|
|
49
65
|
for (let i = 0; i < argv.length; i++) {
|
|
50
66
|
const a = argv[i];
|
|
51
67
|
if (i === 0 && a === "init") out.init = true;
|
|
68
|
+
else if (i === 0 && a === "hook") out.hook = true;
|
|
69
|
+
else if (a === "--gateway-server" && out.hook) out.gatewayServer = argv[++i];
|
|
52
70
|
else if (a === "-h" || a === "--help") out.help = true;
|
|
53
71
|
else if (a === "-v" || a === "--version") out.version = true;
|
|
54
72
|
else if (a === "-c" || a === "--config") out.config = argv[++i];
|
|
@@ -67,6 +85,7 @@ async function main(): Promise<void> {
|
|
|
67
85
|
if (args.version) return void console.log(GATEWAY_VERSION);
|
|
68
86
|
if (!args.config) throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
|
|
69
87
|
if (args.init) return init(args.config, args.write === true);
|
|
88
|
+
if (args.hook) return hook(args.config, args.gatewayServer);
|
|
70
89
|
|
|
71
90
|
const loaded = await loadGatewayConfig(args.config);
|
|
72
91
|
if (args.audit) loaded.gateway.mode = "audit";
|
|
@@ -110,6 +129,19 @@ function summarize(r: InitResult, path: string, write: boolean): string {
|
|
|
110
129
|
return `${lines.join("\n")}\n${tail}`;
|
|
111
130
|
}
|
|
112
131
|
|
|
132
|
+
async function readStdin(): Promise<string> {
|
|
133
|
+
const chunks: Buffer[] = [];
|
|
134
|
+
for await (const c of process.stdin) chunks.push(c as Buffer);
|
|
135
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function hook(config: string, gatewayServer?: string): Promise<void> {
|
|
139
|
+
const out = await runHook({ configPath: config, stdin: await readStdin(), ...(gatewayServer ? { gatewayServer } : {}) });
|
|
140
|
+
if (out.stdout) process.stdout.write(`${JSON.stringify(out.stdout)}\n`);
|
|
141
|
+
if (out.stderr) process.stderr.write(`${out.stderr}\n`);
|
|
142
|
+
process.exitCode = out.exitCode;
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
async function init(path: string, write: boolean): Promise<void> {
|
|
114
146
|
const result = await runInit({ path, write });
|
|
115
147
|
if (!write) process.stdout.write(result.text);
|
|
@@ -120,5 +152,6 @@ async function init(path: string, write: boolean): Promise<void> {
|
|
|
120
152
|
main().catch((err: unknown) => {
|
|
121
153
|
const known = err instanceof GatewayConfigError || err instanceof EnvExpansionError;
|
|
122
154
|
console.error(known ? `[adtl-gateway] ${(err as Error).message}` : err);
|
|
123
|
-
|
|
155
|
+
// For Claude Code hooks only exit code 2 stops the call; any other code lets it run.
|
|
156
|
+
process.exit(process.argv[2] === "hook" ? 2 : 1);
|
|
124
157
|
});
|
package/src/gateway.ts
CHANGED
|
@@ -51,7 +51,7 @@ import type { AuditEntry, AuditSink } from "./audit.js";
|
|
|
51
51
|
import { TOOL_SEPARATOR } from "./config.js";
|
|
52
52
|
import type { Blocked, ChainOutcome, InterceptorInvoker } from "./invoker.js";
|
|
53
53
|
|
|
54
|
-
export const GATEWAY_VERSION = "0.3.
|
|
54
|
+
export const GATEWAY_VERSION = "0.3.1";
|
|
55
55
|
|
|
56
56
|
/** `_meta` key on gateway-generated error results describing why a call was stopped. */
|
|
57
57
|
export const GATEWAY_DECISION_KEY = "adtl/gateway";
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code hooks adapter: puts Claude Code's built-in tools (WebFetch, Read,
|
|
3
|
+
* Bash, …) and MCP servers that don't go through the gateway under the same
|
|
4
|
+
* labels and policy as gateway calls.
|
|
5
|
+
*
|
|
6
|
+
* PreToolUse runs the policy against the session label:
|
|
7
|
+
* block → deny, ask → Claude Code's permission prompt,
|
|
8
|
+
* allow → no answer, so Claude Code's own permissions apply
|
|
9
|
+
* PostToolUse labels the tool's output and adds it to the session label
|
|
10
|
+
* PostToolUseFailure same: a failed tool's error output still reaches the model
|
|
11
|
+
*
|
|
12
|
+
* Built-in tools are labeled as the server `claude-code` (see preset.ts); MCP
|
|
13
|
+
* tools `mcp__<server>__<tool>` as `<server>`. Tools of the gateway's own MCP
|
|
14
|
+
* server (`mcp__adtl__*` by default) are skipped: the gateway checks those.
|
|
15
|
+
*
|
|
16
|
+
* Hooks and gateway share one session label through a FileSessionStore keyed
|
|
17
|
+
* by Claude Code's session id, which the gateway reads from
|
|
18
|
+
* `CLAUDE_CODE_SESSION_ID`.
|
|
19
|
+
*
|
|
20
|
+
* Errors fail closed: a PreToolUse hook that can't decide denies the call.
|
|
21
|
+
*/
|
|
22
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
23
|
+
import {
|
|
24
|
+
callHash,
|
|
25
|
+
evaluatePolicy,
|
|
26
|
+
labelResult,
|
|
27
|
+
toolEgress,
|
|
28
|
+
type AdtlConfig,
|
|
29
|
+
type Decision,
|
|
30
|
+
type PolicyDecision,
|
|
31
|
+
type SessionStore,
|
|
32
|
+
} from "@adtl/labels";
|
|
33
|
+
import type { AuditEntry, AuditSink } from "../audit.js";
|
|
34
|
+
import { CLAUDE_CODE_PRESET, CLAUDE_CODE_SERVER } from "./preset.js";
|
|
35
|
+
|
|
36
|
+
export interface HookInput {
|
|
37
|
+
session_id?: string;
|
|
38
|
+
hook_event_name?: string;
|
|
39
|
+
tool_name?: string;
|
|
40
|
+
tool_input?: unknown;
|
|
41
|
+
tool_response?: unknown;
|
|
42
|
+
/** PostToolUseFailure: the error the tool returned. */
|
|
43
|
+
error?: unknown;
|
|
44
|
+
cwd?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HookOptions {
|
|
48
|
+
config: AdtlConfig;
|
|
49
|
+
sessions: SessionStore;
|
|
50
|
+
/** Name of the gateway in Claude Code's MCP config; its tools are skipped. Default `adtl`. */
|
|
51
|
+
gatewayServer?: string;
|
|
52
|
+
/** `audit`: decide and log, but never deny or ask. */
|
|
53
|
+
mode?: "active" | "audit";
|
|
54
|
+
audit?: AuditSink;
|
|
55
|
+
/** Files the agent must not change: the ADTL config and the session directory. */
|
|
56
|
+
protect?: { configPath?: string; sessionsDir?: string };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What the hook process prints and exits with. */
|
|
60
|
+
export interface HookOutcome {
|
|
61
|
+
stdout?: Record<string, unknown>;
|
|
62
|
+
stderr?: string;
|
|
63
|
+
exitCode: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const NOTHING: HookOutcome = { exitCode: 0 };
|
|
67
|
+
|
|
68
|
+
/** `config` with labels for Claude Code's built-in tools; rules under `servers.claude-code` win. */
|
|
69
|
+
export function withClaudeCodePreset(config: AdtlConfig): AdtlConfig {
|
|
70
|
+
const own = config.servers[CLAUDE_CODE_SERVER];
|
|
71
|
+
return {
|
|
72
|
+
...config,
|
|
73
|
+
servers: {
|
|
74
|
+
...config.servers,
|
|
75
|
+
[CLAUDE_CODE_SERVER]: {
|
|
76
|
+
acceptServerClaims: own?.acceptServerClaims ?? false,
|
|
77
|
+
...(own?.defaults ? { defaults: own.defaults } : {}),
|
|
78
|
+
tools: { ...CLAUDE_CODE_PRESET, ...own?.tools },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Which ADTL server and tool a Claude Code tool name is, or undefined for the gateway's own tools. */
|
|
85
|
+
export function routeTool(toolName: string, gatewayServer = "adtl"): { server: string; tool: string } | undefined {
|
|
86
|
+
const m = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
87
|
+
if (!m) return { server: CLAUDE_CODE_SERVER, tool: toolName };
|
|
88
|
+
if (m[1] === gatewayServer) return undefined;
|
|
89
|
+
return { server: m[1]!, tool: m[2]! };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
93
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inside(path: string, dir: string): boolean {
|
|
97
|
+
const r = relative(dir, path);
|
|
98
|
+
return r === "" || (!r.startsWith(`..${sep}`) && r !== ".." && !isAbsolute(r));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A best-effort guard against the agent clearing its own taint by editing the
|
|
103
|
+
* config or deleting session files. Not a sandbox: Bash can always find a way.
|
|
104
|
+
*/
|
|
105
|
+
function protectedTarget(input: HookInput, route: { server: string; tool: string }, protect: HookOptions["protect"]): { decision: Decision; reason: string } | undefined {
|
|
106
|
+
if (!protect || route.server !== CLAUDE_CODE_SERVER) return undefined;
|
|
107
|
+
const args = isRecord(input.tool_input) ? input.tool_input : {};
|
|
108
|
+
const cwd = input.cwd ?? process.cwd();
|
|
109
|
+
const path = typeof args.file_path === "string" ? args.file_path : typeof args.notebook_path === "string" ? args.notebook_path : undefined;
|
|
110
|
+
if (path !== undefined && ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(route.tool)) {
|
|
111
|
+
const abs = resolve(cwd, path);
|
|
112
|
+
if (protect.configPath && abs === resolve(protect.configPath)) {
|
|
113
|
+
return { decision: "block", reason: "The agent may not change the ADTL config; edit it yourself" };
|
|
114
|
+
}
|
|
115
|
+
if (protect.sessionsDir && inside(abs, resolve(protect.sessionsDir))) {
|
|
116
|
+
return { decision: "block", reason: "The agent may not change ADTL session labels" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const command = typeof args.command === "string" ? args.command : undefined;
|
|
120
|
+
if (command !== undefined && ["Bash", "Monitor"].includes(route.tool)) {
|
|
121
|
+
const marks = [".adtl/sessions", protect.sessionsDir].filter((m): m is string => !!m);
|
|
122
|
+
if (marks.some((m) => command.includes(m))) {
|
|
123
|
+
return { decision: "ask", reason: "This command touches ADTL session labels" };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function handleHook(input: HookInput, opts: HookOptions): HookOutcome {
|
|
130
|
+
const event = input.hook_event_name;
|
|
131
|
+
if (event !== "PreToolUse" && event !== "PostToolUse" && event !== "PostToolUseFailure") return NOTHING;
|
|
132
|
+
if (typeof input.tool_name !== "string" || typeof input.session_id !== "string" || input.session_id === "") {
|
|
133
|
+
return failure(event, "hook input has no tool_name or session_id");
|
|
134
|
+
}
|
|
135
|
+
const route = routeTool(input.tool_name, opts.gatewayServer);
|
|
136
|
+
if (!route) return NOTHING;
|
|
137
|
+
const config = withClaudeCodePreset(opts.config);
|
|
138
|
+
const ts = new Date().toISOString();
|
|
139
|
+
const base = { ts, sessionId: input.session_id, event: "tools/call" as const, ...route, via: "claude-code-hook" as const };
|
|
140
|
+
|
|
141
|
+
if (event !== "PreToolUse") {
|
|
142
|
+
const label = labelResult({ ...route, result: input.tool_response ?? input.error ?? null }, config);
|
|
143
|
+
const sessionLabel = opts.sessions.observe(input.session_id, label);
|
|
144
|
+
opts.audit?.write({ ...base, outcome: "forwarded", label, sessionLabel, durationMs: 0 });
|
|
145
|
+
return NOTHING;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hash = callHash(route.server, route.tool, input.tool_input ?? {});
|
|
149
|
+
const session = opts.sessions.get(input.session_id);
|
|
150
|
+
const guard = protectedTarget(input, route, opts.protect);
|
|
151
|
+
const policy: PolicyDecision = evaluatePolicy(config.policy, {
|
|
152
|
+
session,
|
|
153
|
+
...route,
|
|
154
|
+
egress: toolEgress(config, route.server, route.tool),
|
|
155
|
+
});
|
|
156
|
+
const d: PolicyDecision =
|
|
157
|
+
guard && (guard.decision === "block" || policy.decision === "allow")
|
|
158
|
+
? { decision: guard.decision, reason: guard.reason, matched: ["adtl-self-protection"] }
|
|
159
|
+
: policy;
|
|
160
|
+
if (d.decision === "allow") return NOTHING;
|
|
161
|
+
|
|
162
|
+
const audit = opts.mode === "audit";
|
|
163
|
+
const entry: AuditEntry = {
|
|
164
|
+
...base,
|
|
165
|
+
outcome: audit ? "forwarded" : d.decision === "ask" ? "asked" : "blocked",
|
|
166
|
+
decision: d.decision,
|
|
167
|
+
...(audit ? { mode: "audit" as const } : {}),
|
|
168
|
+
reason: d.reason,
|
|
169
|
+
matched: d.matched,
|
|
170
|
+
callHash: hash,
|
|
171
|
+
sessionLabel: session,
|
|
172
|
+
durationMs: 0,
|
|
173
|
+
};
|
|
174
|
+
opts.audit?.write(entry);
|
|
175
|
+
if (audit) return NOTHING;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
exitCode: 0,
|
|
179
|
+
stdout: {
|
|
180
|
+
hookSpecificOutput: {
|
|
181
|
+
hookEventName: "PreToolUse",
|
|
182
|
+
permissionDecision: d.decision === "ask" ? "ask" : "deny",
|
|
183
|
+
permissionDecisionReason:
|
|
184
|
+
d.decision === "ask" ? `ADTL: ${d.reason}. Approve only if you expected this call.` : `ADTL blocked this call: ${d.reason}.`,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Fail closed: exit 2 stops a PreToolUse call and shows the reason to Claude. */
|
|
191
|
+
export function failure(event: string | undefined, why: string): HookOutcome {
|
|
192
|
+
const stopped = event === "PreToolUse" ? " The call was stopped." : "";
|
|
193
|
+
return { exitCode: 2, stderr: `ADTL hook error: ${why}.${stopped}` };
|
|
194
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Labels for Claude Code's built-in tools, used when `servers.claude-code`
|
|
3
|
+
* doesn't override them. Built-in tools are labeled as the server `claude-code`.
|
|
4
|
+
*
|
|
5
|
+
* - Local reads (Read, Grep, Glob) are trusted but private: your own files.
|
|
6
|
+
* Files written from untrusted content aren't tracked until the v0.4 ledger.
|
|
7
|
+
* - Web tools bring in untrusted content, and their URL or query can carry
|
|
8
|
+
* data out, so their egress is external.
|
|
9
|
+
* - Bash can do anything: its output has unknown integrity, and its egress is
|
|
10
|
+
* unknown.
|
|
11
|
+
* - Edits and bookkeeping tools return nothing worth labeling.
|
|
12
|
+
*/
|
|
13
|
+
import type { ToolRule } from "@adtl/labels";
|
|
14
|
+
|
|
15
|
+
export const CLAUDE_CODE_SERVER = "claude-code";
|
|
16
|
+
|
|
17
|
+
const localRead: ToolRule = { integrity: "trusted", confidentiality: "private", egress: "none" };
|
|
18
|
+
const localWrite: ToolRule = { integrity: "trusted", confidentiality: "public", egress: "none" };
|
|
19
|
+
const web: ToolRule = { integrity: "untrusted", confidentiality: "public", egress: "external" };
|
|
20
|
+
const shell: ToolRule = { integrity: "unknown", confidentiality: "private", egress: "unknown" };
|
|
21
|
+
|
|
22
|
+
export const CLAUDE_CODE_PRESET: Record<string, ToolRule> = {
|
|
23
|
+
Read: localRead,
|
|
24
|
+
Grep: localRead,
|
|
25
|
+
Glob: localRead,
|
|
26
|
+
LS: localRead,
|
|
27
|
+
NotebookRead: localRead,
|
|
28
|
+
Write: localWrite,
|
|
29
|
+
Edit: localWrite,
|
|
30
|
+
MultiEdit: localWrite,
|
|
31
|
+
NotebookEdit: localWrite,
|
|
32
|
+
TodoWrite: localWrite,
|
|
33
|
+
AskUserQuestion: localWrite,
|
|
34
|
+
Skill: localWrite,
|
|
35
|
+
ToolSearch: localWrite,
|
|
36
|
+
EnterPlanMode: localWrite,
|
|
37
|
+
ExitPlanMode: localWrite,
|
|
38
|
+
WebFetch: web,
|
|
39
|
+
WebSearch: web,
|
|
40
|
+
Bash: shell,
|
|
41
|
+
BashOutput: shell,
|
|
42
|
+
KillShell: localWrite,
|
|
43
|
+
Monitor: shell,
|
|
44
|
+
// A sub-agent's own tool calls go through these hooks too; its answer mixes what it saw.
|
|
45
|
+
Agent: { integrity: "unknown", confidentiality: "private", egress: "none" },
|
|
46
|
+
Task: { integrity: "unknown", confidentiality: "private", egress: "none" },
|
|
47
|
+
};
|
package/src/hooks/run.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `adtl-gateway hook --config adtl.yaml`: the command Claude Code runs for
|
|
3
|
+
* PreToolUse, PostToolUse and PostToolUseFailure. Reads the hook event from
|
|
4
|
+
* stdin and answers on stdout (JSON) or stderr (with exit code 2).
|
|
5
|
+
*
|
|
6
|
+
* The config needs no `gateway:` section; if there is one, its `audit` log and
|
|
7
|
+
* `mode` are used, so hook and gateway write to the same audit log.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import { FileSessionStore, defaultSessionsDir } from "@adtl/labels";
|
|
12
|
+
import { parseConfigText } from "@adtl/interceptors";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import { JsonlAuditLog } from "../audit.js";
|
|
15
|
+
import { failure, handleHook, type HookInput, type HookOutcome } from "./claudeCode.js";
|
|
16
|
+
|
|
17
|
+
export interface RunHookOptions {
|
|
18
|
+
configPath: string;
|
|
19
|
+
stdin: string;
|
|
20
|
+
gatewayServer?: string;
|
|
21
|
+
env?: NodeJS.ProcessEnv;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runHook(opts: RunHookOptions): Promise<HookOutcome> {
|
|
25
|
+
let input: HookInput;
|
|
26
|
+
try {
|
|
27
|
+
input = JSON.parse(opts.stdin) as HookInput;
|
|
28
|
+
} catch {
|
|
29
|
+
// Without the event name we can't tell whether this is a PreToolUse call; refuse it to be safe.
|
|
30
|
+
return failure("PreToolUse", "hook input on stdin is not JSON");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const path = resolve(input.cwd ?? process.cwd(), opts.configPath);
|
|
34
|
+
const text = await readFile(path, "utf8");
|
|
35
|
+
const config = parseConfigText(text, path);
|
|
36
|
+
const gw = ((parseYaml(text) ?? {}) as { gateway?: { audit?: unknown; mode?: unknown } }).gateway ?? {};
|
|
37
|
+
const sessionsDir = defaultSessionsDir(opts.env ?? process.env);
|
|
38
|
+
return handleHook(input, {
|
|
39
|
+
config,
|
|
40
|
+
sessions: new FileSessionStore(sessionsDir),
|
|
41
|
+
...(opts.gatewayServer ? { gatewayServer: opts.gatewayServer } : {}),
|
|
42
|
+
mode: gw.mode === "audit" ? "audit" : "active",
|
|
43
|
+
...(typeof gw.audit === "string" ? { audit: new JsonlAuditLog(resolve(dirname(path), gw.audit)) } : {}),
|
|
44
|
+
protect: { configPath: path, sessionsDir },
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return failure(input.hook_event_name, (err as Error).message);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
package/src/startup.ts
CHANGED
|
@@ -9,7 +9,7 @@ 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 type
|
|
12
|
+
import { defaultSessionsDir, type AdtlConfig } from "@adtl/labels";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { ZodError } from "zod";
|
|
15
15
|
import { ApprovalBroker, type Notifier } from "./approvals.js";
|
|
@@ -117,6 +117,16 @@ export function createApprovals(cfg: GatewayConfig, env: NodeJS.ProcessEnv, log:
|
|
|
117
117
|
return new ApprovalBroker({ notifiers, timeoutMs: a.timeoutSeconds * 1000, log });
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
122
|
+
* servers, the gateway uses that id and keeps session labels in files, so the
|
|
123
|
+
* Claude Code hooks (`adtl-gateway hook`) add to the same session label.
|
|
124
|
+
*/
|
|
125
|
+
export function sharedSession(env: NodeJS.ProcessEnv): { sessionId: string; dir: string } | undefined {
|
|
126
|
+
const sessionId = env.CLAUDE_CODE_SESSION_ID?.trim();
|
|
127
|
+
return sessionId ? { sessionId, dir: defaultSessionsDir(env) } : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
export interface StartedGateway {
|
|
121
131
|
gateway: AdtlGateway;
|
|
122
132
|
close(): Promise<void>;
|
|
@@ -134,9 +144,15 @@ export async function startGateway(
|
|
|
134
144
|
|
|
135
145
|
try {
|
|
136
146
|
// 1. Interceptor server (out of process, per the Interceptors WG).
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
const shared = sharedSession(env);
|
|
148
|
+
const custom = loaded.gateway.interceptors && expandUpstream("interceptors", loaded.gateway.interceptors, env);
|
|
149
|
+
const icfg: StdioUpstream = custom
|
|
150
|
+
? { ...custom, env: { ...custom.env, ...(shared ? { ADTL_SESSIONS_DIR: shared.dir } : {}) } }
|
|
151
|
+
: {
|
|
152
|
+
command: process.execPath,
|
|
153
|
+
args: [bundledInterceptorsCli(), "--config", loaded.path, ...(shared ? ["--sessions-dir", shared.dir] : [])],
|
|
154
|
+
env: {},
|
|
155
|
+
};
|
|
140
156
|
const interceptors = await connectUpstream("interceptors", icfg, baseDir);
|
|
141
157
|
clients.push(interceptors);
|
|
142
158
|
if (!interceptors.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
|
|
@@ -152,6 +168,7 @@ export async function startGateway(
|
|
|
152
168
|
const audit = loaded.gateway.mode === "audit";
|
|
153
169
|
const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined);
|
|
154
170
|
log.info(`interceptors: ${names.join(", ")}`);
|
|
171
|
+
if (shared) log.info(`sharing session labels with Claude Code hooks (${shared.dir})`);
|
|
155
172
|
if (audit) log.warn("audit mode: calls are logged but never stopped or changed");
|
|
156
173
|
|
|
157
174
|
// 2. Upstream servers. One failing upstream does not stop the others.
|
|
@@ -178,6 +195,7 @@ export async function startGateway(
|
|
|
178
195
|
const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
|
|
179
196
|
const approvals = createApprovals(loaded.gateway, env, log);
|
|
180
197
|
const gateway = new AdtlGateway({
|
|
198
|
+
...(shared ? { sessionId: shared.sessionId } : {}),
|
|
181
199
|
upstreams,
|
|
182
200
|
invoker,
|
|
183
201
|
...(auditLog ? { audit: auditLog } : {}),
|