@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.
Files changed (50) hide show
  1. package/README.md +90 -9
  2. package/dist/audit.d.ts +3 -1
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js.map +1 -1
  5. package/dist/config.d.ts +190 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +29 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/gateway.d.ts +7 -1
  10. package/dist/gateway.d.ts.map +1 -1
  11. package/dist/gateway.js +10 -4
  12. package/dist/gateway.js.map +1 -1
  13. package/dist/hooks/agentLinks.d.ts +18 -0
  14. package/dist/hooks/agentLinks.d.ts.map +1 -0
  15. package/dist/hooks/agentLinks.js +48 -0
  16. package/dist/hooks/agentLinks.js.map +1 -0
  17. package/dist/hooks/claudeCode.d.ts +3 -0
  18. package/dist/hooks/claudeCode.d.ts.map +1 -1
  19. package/dist/hooks/claudeCode.js +27 -7
  20. package/dist/hooks/claudeCode.js.map +1 -1
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.d.ts.map +1 -1
  23. package/dist/hooks/index.js +1 -0
  24. package/dist/hooks/index.js.map +1 -1
  25. package/dist/hooks/run.d.ts.map +1 -1
  26. package/dist/hooks/run.js +3 -1
  27. package/dist/hooks/run.js.map +1 -1
  28. package/dist/init/suggest.d.ts +3 -1
  29. package/dist/init/suggest.d.ts.map +1 -1
  30. package/dist/init/suggest.js +8 -0
  31. package/dist/init/suggest.js.map +1 -1
  32. package/dist/invoker.d.ts +54 -6
  33. package/dist/invoker.d.ts.map +1 -1
  34. package/dist/invoker.js +115 -19
  35. package/dist/invoker.js.map +1 -1
  36. package/dist/startup.d.ts +9 -0
  37. package/dist/startup.d.ts.map +1 -1
  38. package/dist/startup.js +56 -6
  39. package/dist/startup.js.map +1 -1
  40. package/package.json +4 -4
  41. package/src/audit.ts +3 -1
  42. package/src/config.ts +31 -0
  43. package/src/gateway.ts +12 -4
  44. package/src/hooks/agentLinks.ts +58 -0
  45. package/src/hooks/claudeCode.ts +31 -7
  46. package/src/hooks/index.ts +1 -0
  47. package/src/hooks/run.ts +3 -1
  48. package/src/init/suggest.ts +10 -1
  49. package/src/invoker.ts +146 -21
  50. 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 bound = await InterceptorInvoker.discover(interceptors);
241
- const names = bound.map((b) => b.definition.name);
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 (!names.includes(required)) {
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 } : {}),