@adtl/gateway 0.7.0 → 0.8.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 +90 -9
- package/dist/audit.d.ts +3 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/config.d.ts +190 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -1
- package/dist/gateway.d.ts +7 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +10 -4
- package/dist/gateway.js.map +1 -1
- package/dist/hooks/agentLinks.d.ts +18 -0
- package/dist/hooks/agentLinks.d.ts.map +1 -0
- package/dist/hooks/agentLinks.js +48 -0
- package/dist/hooks/agentLinks.js.map +1 -0
- package/dist/hooks/claudeCode.d.ts +3 -0
- package/dist/hooks/claudeCode.d.ts.map +1 -1
- package/dist/hooks/claudeCode.js +27 -7
- package/dist/hooks/claudeCode.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/run.d.ts.map +1 -1
- package/dist/hooks/run.js +3 -1
- package/dist/hooks/run.js.map +1 -1
- package/dist/init/suggest.d.ts +3 -1
- package/dist/init/suggest.d.ts.map +1 -1
- package/dist/init/suggest.js +8 -0
- package/dist/init/suggest.js.map +1 -1
- package/dist/invoker.d.ts +54 -6
- package/dist/invoker.d.ts.map +1 -1
- package/dist/invoker.js +115 -19
- package/dist/invoker.js.map +1 -1
- package/dist/startup.d.ts +9 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +56 -6
- package/dist/startup.js.map +1 -1
- package/package.json +4 -4
- package/src/audit.ts +3 -1
- package/src/config.ts +31 -0
- package/src/gateway.ts +12 -4
- package/src/hooks/agentLinks.ts +58 -0
- package/src/hooks/claudeCode.ts +31 -7
- package/src/hooks/index.ts +1 -0
- package/src/hooks/run.ts +3 -1
- package/src/init/suggest.ts +10 -1
- package/src/invoker.ts +146 -21
- package/src/startup.ts +53 -6
package/src/startup.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "./config.js";
|
|
29
29
|
import { EmailNotifier } from "./email.js";
|
|
30
30
|
import { AdtlGateway, GATEWAY_VERSION, type GatewayLogger } from "./gateway.js";
|
|
31
|
-
import { InterceptorInvoker } from "./invoker.js";
|
|
31
|
+
import { InterceptorInvoker, checkOverrides, duplicateInterceptors, mutatorsAfter } from "./invoker.js";
|
|
32
32
|
import { NtfyNotifier } from "./ntfy.js";
|
|
33
33
|
import { RoleAuthorizer } from "./roles.js";
|
|
34
34
|
|
|
@@ -192,12 +192,23 @@ export async function gatewayAuthorizers(cfg: GatewayConfig, baseDir: string): P
|
|
|
192
192
|
* Under Claude Code, which passes `CLAUDE_CODE_SESSION_ID` to stdio MCP
|
|
193
193
|
* servers, the gateway uses that id and keeps session labels in files, so the
|
|
194
194
|
* Claude Code hooks (`adtl-gateway hook`) add to the same session label.
|
|
195
|
+
*
|
|
196
|
+
* `ADTL_SESSION_ID` does the same for any agent framework, and wins over
|
|
197
|
+
* Claude Code's: a sub-agent's gateway started with its parent's session id
|
|
198
|
+
* starts from the parent's label, and what it reads taints the parent too.
|
|
195
199
|
*/
|
|
196
200
|
export function sharedSession(env: NodeJS.ProcessEnv): { sessionId: string; dir: string } | undefined {
|
|
197
|
-
const sessionId = env.CLAUDE_CODE_SESSION_ID?.trim();
|
|
201
|
+
const sessionId = env.ADTL_SESSION_ID?.trim() || env.CLAUDE_CODE_SESSION_ID?.trim();
|
|
198
202
|
return sessionId ? { sessionId, dir: defaultSessionsDir(env) } : undefined;
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
/** `ADTL_AGENT_ID` (and `ADTL_AGENT_TYPE`): which agent this gateway serves, for the audit log. */
|
|
206
|
+
export function gatewayAgent(env: NodeJS.ProcessEnv): { id: string; type?: string } | undefined {
|
|
207
|
+
const id = env.ADTL_AGENT_ID?.trim();
|
|
208
|
+
const type = env.ADTL_AGENT_TYPE?.trim();
|
|
209
|
+
return id ? { id, ...(type ? { type } : {}) } : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
201
212
|
export interface StartedGateway {
|
|
202
213
|
gateway: AdtlGateway;
|
|
203
214
|
close(): Promise<void>;
|
|
@@ -237,15 +248,48 @@ export async function startGateway(
|
|
|
237
248
|
if (!interceptors.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
|
|
238
249
|
throw new GatewayConfigError(`The interceptor server does not declare the ${INTERCEPTORS_EXTENSION} extension`);
|
|
239
250
|
}
|
|
240
|
-
const
|
|
241
|
-
const
|
|
251
|
+
const own = await InterceptorInvoker.discover(interceptors, { server: "interceptors" });
|
|
252
|
+
const ownNames = own.map((b) => b.definition.name);
|
|
242
253
|
for (const required of [POLICY_INTERCEPTOR, LABEL_INTERCEPTOR]) {
|
|
243
|
-
if (!
|
|
254
|
+
if (!ownNames.includes(required)) {
|
|
244
255
|
throw new GatewayConfigError(`The interceptor server does not offer ${required}; refusing to start without it`);
|
|
245
256
|
}
|
|
246
257
|
}
|
|
258
|
+
// More interceptor servers (SEP-2624: interceptors may come from N servers, each invoked where it was advertised).
|
|
259
|
+
// A server that can't be reached stops startup: the owner asked for its interceptors.
|
|
260
|
+
const bound = [...own];
|
|
261
|
+
for (const [id, server] of Object.entries(loaded.gateway.interceptorServers)) {
|
|
262
|
+
const where = `gateway.interceptorServers.${id}`;
|
|
263
|
+
let client: Client;
|
|
264
|
+
try {
|
|
265
|
+
client = await connectUpstream(`interceptors:${id}`, expandUpstream(id, server, env), baseDir);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
throw new GatewayConfigError(`${where}: could not connect: ${err instanceof Error ? err.message : String(err)}`);
|
|
268
|
+
}
|
|
269
|
+
clients.push(client);
|
|
270
|
+
if (!client.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
|
|
271
|
+
throw new GatewayConfigError(`${where} does not declare the ${INTERCEPTORS_EXTENSION} extension`);
|
|
272
|
+
}
|
|
273
|
+
bound.push(...(await InterceptorInvoker.discover(client, { server: id, sendConfig: false })));
|
|
274
|
+
}
|
|
275
|
+
const duplicates = duplicateInterceptors(bound);
|
|
276
|
+
if (duplicates.length) throw new GatewayConfigError(`Interceptor names must be unique across servers: ${duplicates.join("; ")}`);
|
|
277
|
+
const names = bound.map((b) => b.definition.name);
|
|
278
|
+
const overrides = loaded.gateway.interceptorOverrides;
|
|
279
|
+
const problems = checkOverrides(bound.map((b) => b.definition), overrides);
|
|
280
|
+
if (problems.length) throw new GatewayConfigError(`gateway.interceptorOverrides: ${problems.join("; ")}`);
|
|
281
|
+
const late = mutatorsAfter(bound, LABEL_INTERCEPTOR, overrides);
|
|
282
|
+
if (late.length) {
|
|
283
|
+
log.warn(`${late.join(", ")} run after ${LABEL_INTERCEPTOR} and could change or drop its label; give them a lower priorityHint`);
|
|
284
|
+
}
|
|
285
|
+
for (const name of [POLICY_INTERCEPTOR, LABEL_INTERCEPTOR]) {
|
|
286
|
+
const o = overrides[name];
|
|
287
|
+
if (o?.mode === "audit" || o?.failOpen === true || o?.hooks) {
|
|
288
|
+
log.warn(`gateway.interceptorOverrides relaxes ${name} (${Object.keys(o).join(", ")}): calls it would stop may get through`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
247
291
|
const audit = loaded.gateway.mode === "audit";
|
|
248
|
-
const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined);
|
|
292
|
+
const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined, overrides);
|
|
249
293
|
log.info(`interceptors: ${names.join(", ")}`);
|
|
250
294
|
if (shared) log.info(`sharing session labels with Claude Code hooks (${shared.dir})`);
|
|
251
295
|
if (audit) log.warn("audit mode: calls are logged but never stopped or changed");
|
|
@@ -280,8 +324,11 @@ export async function startGateway(
|
|
|
280
324
|
if (!principal) log.warn('roles are set but gateway.principal is not: only roles with members ["*"] apply');
|
|
281
325
|
}
|
|
282
326
|
for (const a of authorizers) if (a.name !== "roles") log.info(`authorizer: ${a.name}`);
|
|
327
|
+
const agent = gatewayAgent(env);
|
|
328
|
+
if (agent) log.info(`agent: ${agent.id}${agent.type ? ` (${agent.type})` : ""}`);
|
|
283
329
|
const gateway = new AdtlGateway({
|
|
284
330
|
...(shared ? { sessionId: shared.sessionId } : {}),
|
|
331
|
+
...(agent ? { agent } : {}),
|
|
285
332
|
upstreams,
|
|
286
333
|
invoker,
|
|
287
334
|
...(auditLog ? { audit: auditLog } : {}),
|