@adtl/gateway 0.5.1 → 0.7.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 +152 -9
- package/dist/approvals.d.ts +22 -6
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +56 -26
- package/dist/approvals.js.map +1 -1
- package/dist/audit.d.ts +8 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/auditCli.d.ts +2 -0
- package/dist/auditCli.d.ts.map +1 -1
- package/dist/auditCli.js +13 -10
- package/dist/auditCli.js.map +1 -1
- package/dist/cedar.d.ts +27 -0
- package/dist/cedar.d.ts.map +1 -0
- package/dist/cedar.js +134 -0
- package/dist/cedar.js.map +1 -0
- package/dist/cli.js +17 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +330 -21
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +74 -1
- package/dist/config.js.map +1 -1
- package/dist/elicitation.d.ts +25 -0
- package/dist/elicitation.d.ts.map +1 -0
- package/dist/elicitation.js +46 -0
- package/dist/elicitation.js.map +1 -0
- package/dist/gateway.d.ts +30 -5
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +96 -14
- package/dist/gateway.js.map +1 -1
- package/dist/hooks/claudeCode.d.ts +4 -0
- package/dist/hooks/claudeCode.d.ts.map +1 -1
- package/dist/hooks/claudeCode.js +8 -1
- package/dist/hooks/claudeCode.js.map +1 -1
- package/dist/hooks/run.d.ts.map +1 -1
- package/dist/hooks/run.js +6 -3
- package/dist/hooks/run.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/init/presets.d.ts.map +1 -1
- package/dist/init/presets.js +54 -0
- package/dist/init/presets.js.map +1 -1
- package/dist/invoker.d.ts +21 -1
- package/dist/invoker.d.ts.map +1 -1
- package/dist/invoker.js +19 -2
- package/dist/invoker.js.map +1 -1
- package/dist/ledgerCli.d.ts +1 -1
- package/dist/ledgerCli.d.ts.map +1 -1
- package/dist/ledgerCli.js +9 -3
- package/dist/ledgerCli.js.map +1 -1
- package/dist/ntfy.d.ts +3 -1
- package/dist/ntfy.d.ts.map +1 -1
- package/dist/ntfy.js +2 -1
- package/dist/ntfy.js.map +1 -1
- package/dist/playgroundCli.d.ts +8 -0
- package/dist/playgroundCli.d.ts.map +1 -0
- package/dist/playgroundCli.js +57 -0
- package/dist/playgroundCli.js.map +1 -0
- package/dist/receipt.d.ts +62 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +117 -0
- package/dist/receipt.js.map +1 -0
- package/dist/receiptCli.d.ts +8 -0
- package/dist/receiptCli.d.ts.map +1 -0
- package/dist/receiptCli.js +87 -0
- package/dist/receiptCli.js.map +1 -0
- package/dist/roles.d.ts +9 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +41 -0
- package/dist/roles.js.map +1 -0
- package/dist/startup.d.ts +14 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +92 -7
- package/dist/startup.js.map +1 -1
- package/package.json +16 -4
- package/playground/fake-github.mjs +98 -0
- package/playground/fake-memory.mjs +130 -0
- package/playground/index.html +411 -0
- package/playground/playground.yaml +40 -0
- package/playground/server.mjs +290 -0
- package/src/approvals.ts +63 -26
- package/src/audit.ts +5 -0
- package/src/auditCli.ts +11 -8
- package/src/cedar.ts +140 -0
- package/src/cli.ts +16 -1
- package/src/config.ts +80 -1
- package/src/elicitation.ts +69 -0
- package/src/gateway.ts +108 -17
- package/src/hooks/claudeCode.ts +13 -2
- package/src/hooks/run.ts +6 -3
- package/src/index.ts +6 -0
- package/src/init/presets.ts +54 -0
- package/src/invoker.ts +39 -2
- package/src/ledgerCli.ts +9 -3
- package/src/ntfy.ts +4 -1
- package/src/playgroundCli.ts +60 -0
- package/src/receipt.ts +148 -0
- package/src/receiptCli.ts +85 -0
- package/src/roles.ts +55 -0
- package/src/startup.ts +88 -6
package/src/startup.ts
CHANGED
|
@@ -9,14 +9,17 @@ 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, type AdtlConfig } from "@adtl/labels";
|
|
12
|
+
import { SIGNING_KEY_ENV, 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";
|
|
16
16
|
import { JsonlAuditLog } from "./audit.js";
|
|
17
|
+
import type { Authorizer, Principal } from "./authorizer.js";
|
|
18
|
+
import { CedarAuthorizer } from "./cedar.js";
|
|
17
19
|
import {
|
|
18
20
|
GatewayConfigSchema,
|
|
19
21
|
expandApprovals,
|
|
22
|
+
expandEnv,
|
|
20
23
|
expandUpstream,
|
|
21
24
|
isHttpUpstream,
|
|
22
25
|
type GatewayConfig,
|
|
@@ -27,6 +30,7 @@ import { EmailNotifier } from "./email.js";
|
|
|
27
30
|
import { AdtlGateway, GATEWAY_VERSION, type GatewayLogger } from "./gateway.js";
|
|
28
31
|
import { InterceptorInvoker } from "./invoker.js";
|
|
29
32
|
import { NtfyNotifier } from "./ntfy.js";
|
|
33
|
+
import { RoleAuthorizer } from "./roles.js";
|
|
30
34
|
|
|
31
35
|
export class GatewayConfigError extends Error {
|
|
32
36
|
constructor(message: string) {
|
|
@@ -117,10 +121,71 @@ export function createApprovals(cfg: GatewayConfig, env: NodeJS.ProcessEnv, log:
|
|
|
117
121
|
if (a.email) {
|
|
118
122
|
notifiers.push(new EmailNotifier({ smtp: a.email.smtp, from: a.email.from, to: [a.email.to].flat(), on: a.email.on }));
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
const approverChannels = new Map<string, Notifier[]>(
|
|
125
|
+
Object.entries(a.approvers ?? {}).map(([id, c]) => [id, [new NtfyNotifier({ ...c.ntfy, name: `ntfy:${id}` })]]),
|
|
126
|
+
);
|
|
127
|
+
if (notifiers.length === 0 && approverChannels.size === 0 && !a.elicitation) return undefined;
|
|
128
|
+
if (!notifiers.some((n) => n.listen) && approverChannels.size === 0 && !a.elicitation) {
|
|
129
|
+
log.warn('no approval channel can carry answers (configure gateway.approvals.ntfy); "ask" still blocks');
|
|
130
|
+
}
|
|
131
|
+
const approvers = approverChannels.size ? `; approvers: ${[...approverChannels.keys()].join(", ")}` : "";
|
|
132
|
+
const shared = [...notifiers.map((n) => n.name), ...(a.elicitation ? ["elicitation (if the client supports it)"] : [])];
|
|
133
|
+
log.info(`approvals: ${shared.join(", ") || "none shared"}${approvers} (timeout ${a.timeoutSeconds}s)`);
|
|
134
|
+
return new ApprovalBroker({ notifiers, approverChannels, timeoutMs: a.timeoutSeconds * 1000, log });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check that every role a policy rule names under `approvers:` exists, has
|
|
139
|
+
* named members, and that each member has a channel in
|
|
140
|
+
* `gateway.approvals.approvers`. Returns each such role's members.
|
|
141
|
+
*/
|
|
142
|
+
export function approverRoles(labels: AdtlConfig, cfg: GatewayConfig): Map<string, string[]> {
|
|
143
|
+
const out = new Map<string, string[]>();
|
|
144
|
+
const problems: string[] = [];
|
|
145
|
+
for (const rule of labels.policy.rules) {
|
|
146
|
+
for (const role of rule.approvers ?? []) {
|
|
147
|
+
if (out.has(role)) continue;
|
|
148
|
+
const r = cfg.roles?.[role];
|
|
149
|
+
if (!r) {
|
|
150
|
+
problems.push(`policy rule "${rule.name}" names approver role "${role}", which is not under gateway.roles`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const members = [...r.members];
|
|
154
|
+
if (members.includes("*")) problems.push(`approver role "${role}" has member "*": approvers must be named, so the answer says who approved`);
|
|
155
|
+
for (const m of members.filter((x) => x !== "*")) {
|
|
156
|
+
if (!cfg.approvals?.approvers?.[m]) problems.push(`approver "${m}" (role "${role}") has no channel under gateway.approvals.approvers`);
|
|
157
|
+
}
|
|
158
|
+
out.set(role, members);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (problems.length) throw new GatewayConfigError(`approvers:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `gateway.principal`, with `${VAR}` in its id expanded. */
|
|
166
|
+
export function gatewayPrincipal(cfg: GatewayConfig, env: NodeJS.ProcessEnv): Principal | undefined {
|
|
167
|
+
const p = cfg.principal;
|
|
168
|
+
if (!p) return undefined;
|
|
169
|
+
const id = expandEnv(p.id, env, "gateway.principal.id").trim();
|
|
170
|
+
if (!id) throw new GatewayConfigError("gateway.principal.id is empty after expanding environment variables");
|
|
171
|
+
return { type: p.type, id, ...(p.claims ? { claims: p.claims } : {}) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The configured authorizers, in the order they are asked: roles, then cedar.
|
|
176
|
+
* Cedar files are read and parsed here, so a bad policy fails startup.
|
|
177
|
+
*/
|
|
178
|
+
export async function gatewayAuthorizers(cfg: GatewayConfig, baseDir: string): Promise<Authorizer[]> {
|
|
179
|
+
const out: Authorizer[] = [];
|
|
180
|
+
if (cfg.roles) out.push(new RoleAuthorizer(cfg.roles));
|
|
181
|
+
if (cfg.cedar) {
|
|
182
|
+
try {
|
|
183
|
+
out.push(await CedarAuthorizer.load(cfg.cedar, (p) => resolve(baseDir, p)));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
throw new GatewayConfigError((err as Error).message);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
124
189
|
}
|
|
125
190
|
|
|
126
191
|
/**
|
|
@@ -143,7 +208,10 @@ export async function startGateway(
|
|
|
143
208
|
opts: { env?: NodeJS.ProcessEnv; logger?: GatewayLogger } = {},
|
|
144
209
|
): Promise<StartedGateway> {
|
|
145
210
|
const env = opts.env ?? process.env;
|
|
211
|
+
// Config problems that need no process: fail before starting any.
|
|
212
|
+
const approverMembers = approverRoles(loaded.labels, loaded.gateway);
|
|
146
213
|
const baseDir = dirname(loaded.path);
|
|
214
|
+
const authorizers = await gatewayAuthorizers(loaded.gateway, baseDir);
|
|
147
215
|
const log = opts.logger ?? { info: (m: string) => console.error(`[adtl-gateway] ${m}`), warn: (m: string) => console.error(`[adtl-gateway] warning: ${m}`) };
|
|
148
216
|
const clients: Client[] = [];
|
|
149
217
|
const closeAll = () => Promise.allSettled(clients.map((c) => c.close())).then(() => undefined);
|
|
@@ -153,7 +221,10 @@ export async function startGateway(
|
|
|
153
221
|
const shared = sharedSession(env);
|
|
154
222
|
const custom = loaded.gateway.interceptors && expandUpstream("interceptors", loaded.gateway.interceptors, env);
|
|
155
223
|
// The interceptor process starts with a minimal environment; pass on where the ledger lives.
|
|
156
|
-
const ledgerEnv: Record<string, string> =
|
|
224
|
+
const ledgerEnv: Record<string, string> = {
|
|
225
|
+
...(env.ADTL_LEDGER ? { ADTL_LEDGER: env.ADTL_LEDGER } : {}),
|
|
226
|
+
...(env[SIGNING_KEY_ENV] ? { [SIGNING_KEY_ENV]: env[SIGNING_KEY_ENV]! } : {}),
|
|
227
|
+
};
|
|
157
228
|
const icfg: StdioUpstream = custom
|
|
158
229
|
? { ...custom, env: { ...ledgerEnv, ...custom.env, ...(shared ? { ADTL_SESSIONS_DIR: shared.dir } : {}) } }
|
|
159
230
|
: {
|
|
@@ -202,12 +273,23 @@ export async function startGateway(
|
|
|
202
273
|
|
|
203
274
|
const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
|
|
204
275
|
const approvals = createApprovals(loaded.gateway, env, log);
|
|
276
|
+
const principal = gatewayPrincipal(loaded.gateway, env);
|
|
277
|
+
const roles = loaded.gateway.roles;
|
|
278
|
+
if (roles) {
|
|
279
|
+
log.info(`roles: ${Object.keys(roles).join(", ")} (caller: ${principal?.id ?? "anonymous"})`);
|
|
280
|
+
if (!principal) log.warn('roles are set but gateway.principal is not: only roles with members ["*"] apply');
|
|
281
|
+
}
|
|
282
|
+
for (const a of authorizers) if (a.name !== "roles") log.info(`authorizer: ${a.name}`);
|
|
205
283
|
const gateway = new AdtlGateway({
|
|
206
284
|
...(shared ? { sessionId: shared.sessionId } : {}),
|
|
207
285
|
upstreams,
|
|
208
286
|
invoker,
|
|
209
287
|
...(auditLog ? { audit: auditLog } : {}),
|
|
210
288
|
...(approvals ? { approvals } : {}),
|
|
289
|
+
...(authorizers.length ? { authorizer: authorizers } : {}),
|
|
290
|
+
...(principal ? { principal } : {}),
|
|
291
|
+
...(approverMembers.size ? { approverRoles: approverMembers } : {}),
|
|
292
|
+
...(loaded.gateway.approvals?.elicitation ? { elicitApprovals: true } : {}),
|
|
211
293
|
logger: log,
|
|
212
294
|
});
|
|
213
295
|
return { gateway, close: closeAll };
|