@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/dist/config.js
CHANGED
|
@@ -12,15 +12,22 @@
|
|
|
12
12
|
* docs:
|
|
13
13
|
* url: https://example.com/mcp
|
|
14
14
|
* headers: { Authorization: "Bearer ${DOCS_TOKEN}" }
|
|
15
|
+
* principal: { id: "${USER}" } # optional: who runs this gateway (default: anonymous)
|
|
16
|
+
* roles: # optional: who may use which tools (deny by default)
|
|
17
|
+
* maintainers: { members: [alice], tools: ["github.*"] }
|
|
18
|
+
* readers: { members: ["*"], tools: ["github.read_*", "github.get_*"] }
|
|
19
|
+
* cedar: { policies: ./adtl.cedar } # optional: attribute-based rules (see cedar.ts)
|
|
15
20
|
* approvals: # optional: make "ask" really ask
|
|
16
21
|
* timeoutSeconds: 50
|
|
17
22
|
* ntfy: { topic: "${ADTL_NTFY_TOPIC}" }
|
|
23
|
+
* approvers: # optional: each approver's own channel (policy rule `approvers:`)
|
|
24
|
+
* alice: { ntfy: { topic: "${ALICE_NTFY_TOPIC}" } }
|
|
18
25
|
* email:
|
|
19
26
|
* smtp: { host: smtp.example.com, port: 465, user: "${SMTP_USER}", pass: "${SMTP_PASS}" }
|
|
20
27
|
* from: adtl@example.com
|
|
21
28
|
* to: you@example.com
|
|
22
29
|
*
|
|
23
|
-
* `${VAR}` in command, args, env, cwd, url, headers
|
|
30
|
+
* `${VAR}` in command, args, env, cwd, url, headers, approval settings and principal.id is
|
|
24
31
|
* replaced from the gateway's environment. A missing variable is a config error,
|
|
25
32
|
* unless a default is given: `${VAR:-default}`.
|
|
26
33
|
*/
|
|
@@ -78,6 +85,63 @@ export const ApprovalsConfigSchema = z
|
|
|
78
85
|
timeoutSeconds: z.number().int().positive().max(3600).default(50),
|
|
79
86
|
ntfy: NtfyConfigSchema.optional(),
|
|
80
87
|
email: EmailConfigSchema.optional(),
|
|
88
|
+
/**
|
|
89
|
+
* Also ask in the MCP client (elicitation), when it supports it. Off by
|
|
90
|
+
* default: a client that accepts elicitations by itself would approve calls.
|
|
91
|
+
*/
|
|
92
|
+
elicitation: z.boolean().default(false),
|
|
93
|
+
/**
|
|
94
|
+
* Each approver's own channel, by principal id. Policy rules with
|
|
95
|
+
* `approvers: [role]` ask only these people, so the answer names who approved.
|
|
96
|
+
*/
|
|
97
|
+
approvers: z.record(z.object({ ntfy: NtfyConfigSchema }).strict()).optional(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
/** `server.tool` (or `server.prompt`), with `*` wildcards, as in policy rules. */
|
|
101
|
+
const QualifiedPattern = z
|
|
102
|
+
.string()
|
|
103
|
+
.min(1)
|
|
104
|
+
.refine((p) => /^[^.]+\..+$/.test(p), { message: 'Use "server.name" with optional * wildcards, e.g. "github.read_*" or "github.*"' });
|
|
105
|
+
export const PrincipalConfigSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
type: z.enum(["user", "service"]).default("user"),
|
|
108
|
+
id: z.string().min(1),
|
|
109
|
+
claims: z.record(z.unknown()).optional(),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
export const RoleSchema = z
|
|
113
|
+
.object({
|
|
114
|
+
/** Principal ids; "*" means everyone, including an anonymous caller. */
|
|
115
|
+
members: z.array(z.string().min(1)).nonempty("A role needs at least one member (\"*\" for everyone)"),
|
|
116
|
+
tools: z.array(QualifiedPattern).default([]),
|
|
117
|
+
prompts: z.array(QualifiedPattern).default([]),
|
|
118
|
+
/** Resource URI patterns, e.g. "github://acme/*". */
|
|
119
|
+
resources: z.array(z.string().min(1)).default([]),
|
|
120
|
+
})
|
|
121
|
+
.strict()
|
|
122
|
+
.refine((r) => r.tools.length + r.prompts.length + r.resources.length > 0, {
|
|
123
|
+
message: "A role must grant at least one of tools, prompts or resources",
|
|
124
|
+
});
|
|
125
|
+
export const CedarConfigSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
/** Cedar policy file. Relative paths are resolved against the config file's directory. */
|
|
128
|
+
policies: z.string().min(1),
|
|
129
|
+
/** Optional Cedar entities JSON file: groups (`parents`) and attributes. */
|
|
130
|
+
entities: z.string().min(1).optional(),
|
|
131
|
+
/** IANA time zone for `context.time`, e.g. "Europe/Berlin". Default: the gateway's. */
|
|
132
|
+
timeZone: z
|
|
133
|
+
.string()
|
|
134
|
+
.min(1)
|
|
135
|
+
.refine((tz) => {
|
|
136
|
+
try {
|
|
137
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}, "Unknown time zone")
|
|
144
|
+
.optional(),
|
|
81
145
|
})
|
|
82
146
|
.strict();
|
|
83
147
|
export const GatewayConfigSchema = z
|
|
@@ -101,6 +165,15 @@ export const GatewayConfigSchema = z
|
|
|
101
165
|
approvals: ApprovalsConfigSchema.optional(),
|
|
102
166
|
/** Timeout for each interceptor invocation, in milliseconds. */
|
|
103
167
|
interceptorTimeoutMs: z.number().int().positive().default(5000),
|
|
168
|
+
/** Who runs this gateway. Passed to the authorizer and the interceptors. Default: anonymous. */
|
|
169
|
+
principal: PrincipalConfigSchema.optional(),
|
|
170
|
+
/** Role-based authorization: when present, a call no role grants is denied. */
|
|
171
|
+
roles: z
|
|
172
|
+
.record(RoleSchema)
|
|
173
|
+
.refine((r) => Object.keys(r).length > 0, "roles: needs at least one role (remove it to allow everyone)")
|
|
174
|
+
.optional(),
|
|
175
|
+
/** Cedar authorization. With roles as well, both must allow. */
|
|
176
|
+
cedar: CedarConfigSchema.optional(),
|
|
104
177
|
})
|
|
105
178
|
.strict();
|
|
106
179
|
export const TOOL_SEPARATOR = "__";
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,CAAC;IACN,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC3B,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC;KAChC,MAAM,CAAC;IACN,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC1C,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAKjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC;IACpD,+FAA+F;IAC/F,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,6DAA6D;IAC7D,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACpC,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,IAAI,EAAE,CAAC;SACJ,MAAM,CAAC;QACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;QAC9C,yEAAyE;QACzE,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;QAC9B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;QAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC5B,CAAC;SACD,MAAM,EAAE;IACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvE,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;CAC7E,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC;KACnC,MAAM,CAAC;IACN;;;OAGG;IACH,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACjC,KAAK,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACnC;;;OAGG;IACH,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACvC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CAC9E,CAAC;KACD,MAAM,EAAE,CAAC;AAGZ,kFAAkF;AAClF,MAAM,gBAAgB,GAAG,CAAC;KACvB,MAAM,EAAE;KACR,GAAG,CAAC,CAAC,CAAC;KACN,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,iFAAiF,EAAE,CAAC,CAAC;AAExI,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC;KACnC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACjD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;CACzC,CAAC;KACD,MAAM,EAAE,CAAC;AAGZ,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,wEAAwE;IACxE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,uDAAuD,CAAC;IACrG,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9C,qDAAqD;IACrD,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAClD,CAAC;KACD,MAAM,EAAE;KACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE;IACzE,OAAO,EAAE,+DAA+D;CACzE,CAAC,CAAC;AAGL,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,0FAA0F;IAC1F,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACtC,uFAAuF;IACvF,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE;QACb,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,EAAE,mBAAmB,CAAC;SACtB,QAAQ,EAAE;CACd,CAAC;KACD,MAAM,EAAE,CAAC;AAGZ,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,CAAC;IACN,SAAS,EAAE,CAAC;SACT,MAAM,CAAC,cAAc,CAAC;SACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,0CAA0C,CAAC;SACpF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE;QACjE,OAAO,EAAE,gGAAgG;KAC1G,CAAC;IACJ,8GAA8G;IAC9G,YAAY,EAAE,mBAAmB,CAAC,QAAQ,EAAE;IAC5C,kGAAkG;IAClG,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnC;;;OAGG;IACH,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACnD,uFAAuF;IACvF,SAAS,EAAE,qBAAqB,CAAC,QAAQ,EAAE;IAC3C,gEAAgE;IAChE,oBAAoB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC/D,gGAAgG;IAChG,SAAS,EAAE,qBAAqB,CAAC,QAAQ,EAAE;IAC3C,+EAA+E;IAC/E,KAAK,EAAE,CAAC;SACL,MAAM,CAAC,UAAU,CAAC;SAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,8DAA8D,CAAC;SACxG,QAAQ,EAAE;IACb,gEAAgE;IAChE,KAAK,EAAE,iBAAiB,CAAC,QAAQ,EAAE;CACpC,CAAC;KACD,MAAM,EAAE,CAAC;AAGZ,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACrB;IAArB,YAAqB,QAAgB,EAAE,KAAa;QAClD,KAAK,CAAC,wBAAwB,QAAQ,wBAAwB,KAAK,GAAG,CAAC,CAAC;QADrD,aAAQ,GAAR,QAAQ,CAAQ;QAEnC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,GAAsB,EAAE,KAAa;IAC5E,OAAO,KAAK,CAAC,OAAO,CAAC,oDAAoD,EAAE,CAAC,KAAK,EAAE,MAAc,EAAE,IAAY,EAAE,QAAiB,EAAE,EAAE;QACpI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACpB,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,IAAI,CAAC,KAAK,SAAS;YAAE,MAAM,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9D,OAAO,CAAC,CAAC;IACX,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,GAA2B,EAAE,GAAsB,EAAE,KAAa;IACtF,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1G,CAAC;AAED,MAAM,UAAU,cAAc,CAAqB,EAAU,EAAE,CAAI,EAAE,GAAsB;IACzF,MAAM,KAAK,GAAG,qBAAqB,EAAE,EAAE,CAAC;IACxC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,UAAU,CAAC,EAAE,CAAC;IACzH,CAAC;IACD,OAAO;QACL,GAAG,CAAC;QACJ,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,UAAU,CAAC;QACtD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,SAAS,CAAC,GAAG,CAAC,CAAC;QACpE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;QAC7C,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/E,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,eAAe,CAAC,CAAkB,EAAE,GAAsB;IACxE,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,KAAa,EAAW,EAAE;QAClD,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACxE,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5H,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,CAAC,EAAE,mBAAmB,CAAoB,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAW;IACxC,OAAO,KAAK,IAAI,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approvals inside the MCP client, through `elicitation/create`: the person
|
|
3
|
+
* using the client (for example Claude Code) sees the request and answers
|
|
4
|
+
* Allow or Deny there, no phone needed.
|
|
5
|
+
*
|
|
6
|
+
* The answer is turned into the same signed token a push button would send,
|
|
7
|
+
* so it answers only the one pending call it was asked about. Declining or
|
|
8
|
+
* dismissing the prompt counts as Deny. The gateway uses this channel only
|
|
9
|
+
* when `gateway.approvals.elicitation` is on and the client declares the
|
|
10
|
+
* elicitation capability, and never for rules with named approvers: the
|
|
11
|
+
* client can't say who answered.
|
|
12
|
+
*/
|
|
13
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
+
import type { AnswerFn, ApprovalEvent, ApprovalRequest, Notifier } from "./approvals.js";
|
|
15
|
+
/** True if the connected client can answer elicitation requests. */
|
|
16
|
+
export declare function supportsElicitation(server: Server): boolean;
|
|
17
|
+
export declare class ElicitationChannel implements Notifier {
|
|
18
|
+
private readonly server;
|
|
19
|
+
readonly name = "elicitation";
|
|
20
|
+
private readonly listening;
|
|
21
|
+
constructor(server: Server);
|
|
22
|
+
listen(request: ApprovalRequest, answer: AnswerFn, signal: AbortSignal): Promise<void>;
|
|
23
|
+
notify(event: ApprovalEvent): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=elicitation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elicitation.d.ts","sourceRoot":"","sources":["../src/elicitation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAExE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAOzF,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3D;AAOD,qBAAa,kBAAmB,YAAW,QAAQ;IAIrC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;gBAE7B,MAAM,EAAE,MAAM;IAErC,MAAM,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtF,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAyBlD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ElicitResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/** True if the connected client can answer elicitation requests. */
|
|
3
|
+
export function supportsElicitation(server) {
|
|
4
|
+
return server.getClientCapabilities()?.elicitation !== undefined;
|
|
5
|
+
}
|
|
6
|
+
function message(request) {
|
|
7
|
+
const rule = request.matched.join(", ") || "default";
|
|
8
|
+
return `ADTL: allow ${request.server}.${request.tool}?\n\n${request.reason}\n\nRule: ${rule}. Allow runs this one call; Deny or no answer stops it.`;
|
|
9
|
+
}
|
|
10
|
+
export class ElicitationChannel {
|
|
11
|
+
server;
|
|
12
|
+
name = "elicitation";
|
|
13
|
+
listening = new Map();
|
|
14
|
+
constructor(server) {
|
|
15
|
+
this.server = server;
|
|
16
|
+
}
|
|
17
|
+
async listen(request, answer, signal) {
|
|
18
|
+
this.listening.set(request.id, { answer, signal });
|
|
19
|
+
await new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
20
|
+
this.listening.delete(request.id);
|
|
21
|
+
}
|
|
22
|
+
async notify(event) {
|
|
23
|
+
if (event.kind !== "pending")
|
|
24
|
+
return; // the client sees the call's result; no other notices
|
|
25
|
+
const l = this.listening.get(event.request.id);
|
|
26
|
+
if (!l)
|
|
27
|
+
throw new Error("not listening for this request");
|
|
28
|
+
// A plain request, not Server.elicitInput(): clients that declare only `elicitation: {}` support forms too.
|
|
29
|
+
const result = await this.server.request({
|
|
30
|
+
method: "elicitation/create",
|
|
31
|
+
params: {
|
|
32
|
+
message: message(event.request),
|
|
33
|
+
requestedSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
decision: { type: "string", title: "Decision", enum: ["allow", "deny"], description: "allow runs this one call; deny stops it" },
|
|
37
|
+
},
|
|
38
|
+
required: ["decision"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}, ElicitResultSchema, { signal: l.signal, timeout: Math.max(1, event.request.expiresAt.getTime() - Date.now()) });
|
|
42
|
+
const allow = result.action === "accept" && result.content?.decision === "allow";
|
|
43
|
+
l.answer(allow ? event.tokens.allow : event.tokens.deny);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=elicitation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elicitation.js","sourceRoot":"","sources":["../src/elicitation.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAQxE,oEAAoE;AACpE,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,MAAM,CAAC,qBAAqB,EAAE,EAAE,WAAW,KAAK,SAAS,CAAC;AACnE,CAAC;AAED,SAAS,OAAO,CAAC,OAAwB;IACvC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;IACrD,OAAO,eAAe,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,QAAQ,OAAO,CAAC,MAAM,aAAa,IAAI,yDAAyD,CAAC;AACvJ,CAAC;AAED,MAAM,OAAO,kBAAkB;IAIA;IAHpB,IAAI,GAAG,aAAa,CAAC;IACb,SAAS,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE1D,YAA6B,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAE/C,KAAK,CAAC,MAAM,CAAC,OAAwB,EAAE,MAAgB,EAAE,MAAmB;QAC1E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACxG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAAoB;QAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,sDAAsD;QAC5F,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAC1D,4GAA4G;QAC5G,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CACtC;YACE,MAAM,EAAE,oBAAoB;YAC5B,MAAM,EAAE;gBACN,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;gBAC/B,eAAe,EAAE;oBACf,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE;qBACjI;oBACD,QAAQ,EAAE,CAAC,UAAU,CAAC;iBACvB;aACF;SACF,EACD,kBAAkB,EAClB,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAC3F,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;QACjF,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;CACF"}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ApprovalBroker } from "./approvals.js";
|
|
|
5
5
|
import type { AuditSink } from "./audit.js";
|
|
6
6
|
import { type Authorizer, type Principal } from "./authorizer.js";
|
|
7
7
|
import type { InterceptorInvoker } from "./invoker.js";
|
|
8
|
-
export declare const GATEWAY_VERSION = "0.
|
|
8
|
+
export declare const GATEWAY_VERSION = "0.7.0";
|
|
9
9
|
/** `_meta` key on gateway-generated error results describing why a call was stopped. */
|
|
10
10
|
export declare const GATEWAY_DECISION_KEY = "adtl/gateway";
|
|
11
11
|
export interface GatewayLogger {
|
|
@@ -21,11 +21,20 @@ export interface AdtlGatewayOptions {
|
|
|
21
21
|
logger?: GatewayLogger;
|
|
22
22
|
/** Asks humans about "ask" decisions and sends notices. */
|
|
23
23
|
approvals?: ApprovalBroker;
|
|
24
|
-
/** Decides whether the caller may use a tool, resource or prompt at all, before the policy. */
|
|
25
|
-
authorizer?: Authorizer;
|
|
24
|
+
/** Decides whether the caller may use a tool, resource or prompt at all, before the policy. Several must all allow. */
|
|
25
|
+
authorizer?: Authorizer | Authorizer[];
|
|
26
26
|
/** The caller, passed to the authorizer and the interceptors. Default: anonymous. */
|
|
27
27
|
principal?: Principal;
|
|
28
|
+
/** Members of each role that policy rules name as `approvers:`. */
|
|
29
|
+
approverRoles?: Map<string, string[]>;
|
|
30
|
+
/** Also ask for approval in the MCP client, when it supports elicitation. */
|
|
31
|
+
elicitApprovals?: boolean;
|
|
28
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Who may answer an "ask" whose rules name approver roles: someone in one of
|
|
35
|
+
* the roles of every list. Sorted, for stable notifications and tests.
|
|
36
|
+
*/
|
|
37
|
+
export declare function eligibleApprovers(lists: string[][], roles: Map<string, string[]>): string[];
|
|
29
38
|
export interface CallOptions {
|
|
30
39
|
/** Report progress to the client while a call waits for approval. */
|
|
31
40
|
progress?: (progress: number, message: string) => Promise<void>;
|
|
@@ -37,20 +46,36 @@ export declare class AdtlGateway {
|
|
|
37
46
|
private readonly audit;
|
|
38
47
|
private readonly log;
|
|
39
48
|
private readonly approvals;
|
|
40
|
-
private readonly
|
|
49
|
+
private readonly authorizers;
|
|
41
50
|
private readonly principal;
|
|
51
|
+
private readonly approverRoles;
|
|
52
|
+
private readonly elicitApprovals;
|
|
53
|
+
/** The server the agent is connected to, for elicitation. */
|
|
54
|
+
private server;
|
|
42
55
|
private routes;
|
|
43
56
|
private promptRoutes;
|
|
44
57
|
/** Resource URI → the upstreams that list it. */
|
|
45
58
|
private resourceRoutes;
|
|
46
59
|
private templateRoutes;
|
|
47
60
|
constructor(opts: AdtlGatewayOptions);
|
|
48
|
-
/** Ask
|
|
61
|
+
/** Ask each authorizer in turn. Returns the first denial; in audit mode the caller logs it and carries on. */
|
|
49
62
|
private authorize;
|
|
50
63
|
/** The downstream MCP server the agent connects to. */
|
|
51
64
|
createServer(): Server;
|
|
52
65
|
/** Every upstream's items from one list method, following cursors. Upstreams that fail are skipped with a warning. */
|
|
53
66
|
private listAll;
|
|
67
|
+
/** tools/list as the caller sees it: without the tools the authorizer would deny. */
|
|
68
|
+
visibleTools(): Promise<Tool[]>;
|
|
69
|
+
/** prompts/list as the caller sees it. */
|
|
70
|
+
visiblePrompts(): Promise<Prompt[]>;
|
|
71
|
+
/** resources/list as the caller sees it. A URI several upstreams offer is listed as before (reading it is refused). */
|
|
72
|
+
visibleResources(): Promise<Resource[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Leave out items the authorizer would deny, asked without arguments. Not in
|
|
75
|
+
* audit mode, which changes nothing. Hiding only saves the agent a denied
|
|
76
|
+
* call: routes are kept, and a call to a hidden item is still authorized.
|
|
77
|
+
*/
|
|
78
|
+
private hideDenied;
|
|
54
79
|
listTools(): Promise<Tool[]>;
|
|
55
80
|
listPrompts(): Promise<Prompt[]>;
|
|
56
81
|
listResources(): Promise<Resource[]>;
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAEnE,OAAO,EAUL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EAErB,KAAK,IAAI,EACV,MAAM,oCAAoC,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,gBAAgB,CAAC;AAErE,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAwC,KAAK,UAAU,EAAe,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAErH,OAAO,KAAK,EAAW,kBAAkB,EAAgC,MAAM,cAAc,CAAC;AAE9F,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAuBnD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAOD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,uHAAuH;IACvH,UAAU,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CAAC;IACvC,qFAAqF;IACrF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,6EAA6E;IAC7E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAI3F;AAED,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAmCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwB;IACtD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,6DAA6D;IAC7D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,YAAY,CAAkC;IACtD,iDAAiD;IACjD,OAAO,CAAC,cAAc,CAAkC;IACxD,OAAO,CAAC,cAAc,CAAuB;gBAEjC,IAAI,EAAE,kBAAkB;IAapC,8GAA8G;YAChG,SAAS;IAQvB,uDAAuD;IACvD,YAAY,IAAI,MAAM;IAwCtB,sHAAsH;YACxG,OAAO;IAsBrB,qFAAqF;IAC/E,YAAY,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAQrC,0CAA0C;IACpC,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQzC,uHAAuH;IACjH,gBAAgB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAQ7C;;;;OAIG;YACW,UAAU;IAWlB,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAe5B,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAehC,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAkBpC,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiB1D,wFAAwF;YAC1E,gBAAgB;IAkBxB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQtD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBtF;;;;;OAKG;YACW,cAAc;IA6EtB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,cAAc,CAAC;IAoK5G,gFAAgF;YAClE,eAAe;CAY9B"}
|
package/dist/gateway.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* unanswered or undeliverable calls are stopped. Without an approval channel,
|
|
22
22
|
* "ask" blocks.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* resource or prompt at all;
|
|
24
|
+
* Optional Authorizers decide first whether the caller may use the tool,
|
|
25
|
+
* resource or prompt at all; any denial stops the call before the policy runs.
|
|
26
|
+
* The lists the agent sees leave out what an authorizer would deny, so the
|
|
27
|
+
* agent doesn't try it; hiding is a convenience, and every call is still checked.
|
|
26
28
|
*
|
|
27
29
|
* In audit mode (the invoker runs every interceptor with `mode: "audit"`)
|
|
28
30
|
* nothing is stopped or changed: calls the policy would stop are forwarded,
|
|
@@ -33,15 +35,25 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
33
35
|
import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
|
|
34
36
|
import { CallToolRequestSchema, ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
35
37
|
import { callBindingHash } from "@adtl/labels";
|
|
38
|
+
import { ElicitationChannel, supportsElicitation } from "./elicitation.js";
|
|
36
39
|
import { authorize } from "./authorizer.js";
|
|
37
40
|
import { TOOL_SEPARATOR } from "./config.js";
|
|
38
|
-
export const GATEWAY_VERSION = "0.
|
|
41
|
+
export const GATEWAY_VERSION = "0.7.0";
|
|
39
42
|
/** `_meta` key on gateway-generated error results describing why a call was stopped. */
|
|
40
43
|
export const GATEWAY_DECISION_KEY = "adtl/gateway";
|
|
41
44
|
const stderrLogger = {
|
|
42
45
|
info: (m) => console.error(`[adtl-gateway] ${m}`),
|
|
43
46
|
warn: (m) => console.error(`[adtl-gateway] warning: ${m}`),
|
|
44
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Who may answer an "ask" whose rules name approver roles: someone in one of
|
|
50
|
+
* the roles of every list. Sorted, for stable notifications and tests.
|
|
51
|
+
*/
|
|
52
|
+
export function eligibleApprovers(lists, roles) {
|
|
53
|
+
const sets = lists.map((list) => new Set(list.flatMap((role) => roles.get(role) ?? [])));
|
|
54
|
+
const [first, ...rest] = sets;
|
|
55
|
+
return [...(first ?? [])].filter((id) => id !== "*" && rest.every((s) => s.has(id))).sort();
|
|
56
|
+
}
|
|
45
57
|
/** How often a waiting call reports progress, so clients that reset their timeout on progress keep waiting. */
|
|
46
58
|
const PROGRESS_INTERVAL_MS = 10_000;
|
|
47
59
|
function textError(text, meta) {
|
|
@@ -75,8 +87,12 @@ export class AdtlGateway {
|
|
|
75
87
|
audit;
|
|
76
88
|
log;
|
|
77
89
|
approvals;
|
|
78
|
-
|
|
90
|
+
authorizers;
|
|
79
91
|
principal;
|
|
92
|
+
approverRoles;
|
|
93
|
+
elicitApprovals;
|
|
94
|
+
/** The server the agent is connected to, for elicitation. */
|
|
95
|
+
server;
|
|
80
96
|
routes = new Map();
|
|
81
97
|
promptRoutes = new Map();
|
|
82
98
|
/** Resource URI → the upstreams that list it. */
|
|
@@ -89,12 +105,19 @@ export class AdtlGateway {
|
|
|
89
105
|
this.audit = opts.audit;
|
|
90
106
|
this.log = opts.logger ?? stderrLogger;
|
|
91
107
|
this.approvals = opts.approvals;
|
|
92
|
-
this.
|
|
108
|
+
this.authorizers = opts.authorizer === undefined ? [] : [opts.authorizer].flat();
|
|
93
109
|
this.principal = opts.principal ?? { type: "anonymous" };
|
|
110
|
+
this.approverRoles = opts.approverRoles ?? new Map();
|
|
111
|
+
this.elicitApprovals = opts.elicitApprovals === true;
|
|
94
112
|
}
|
|
95
|
-
/** Ask
|
|
113
|
+
/** Ask each authorizer in turn. Returns the first denial; in audit mode the caller logs it and carries on. */
|
|
96
114
|
async authorize(request) {
|
|
97
|
-
|
|
115
|
+
for (const a of this.authorizers) {
|
|
116
|
+
const denied = await authorize(a, { principal: this.principal, ...request });
|
|
117
|
+
if (denied)
|
|
118
|
+
return denied;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
98
121
|
}
|
|
99
122
|
/** The downstream MCP server the agent connects to. */
|
|
100
123
|
createServer() {
|
|
@@ -110,7 +133,7 @@ export class AdtlGateway {
|
|
|
110
133
|
instructions: "Tools from several MCP servers, named <server>__<tool>. ADTL checks each call against trust labels; " +
|
|
111
134
|
"if a call is stopped, the result explains why.",
|
|
112
135
|
});
|
|
113
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await this.
|
|
136
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await this.visibleTools() }));
|
|
114
137
|
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
115
138
|
const progressToken = req.params._meta?.progressToken;
|
|
116
139
|
const progress = progressToken === undefined
|
|
@@ -119,16 +142,17 @@ export class AdtlGateway {
|
|
|
119
142
|
return this.callTool(req.params.name, req.params.arguments ?? {}, progress ? { progress } : {});
|
|
120
143
|
});
|
|
121
144
|
if (capabilities.resources) {
|
|
122
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await this.
|
|
145
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await this.visibleResources() }));
|
|
123
146
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
124
147
|
resourceTemplates: await this.listResourceTemplates(),
|
|
125
148
|
}));
|
|
126
149
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => this.readResource(req.params.uri));
|
|
127
150
|
}
|
|
128
151
|
if (capabilities.prompts) {
|
|
129
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await this.
|
|
152
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await this.visiblePrompts() }));
|
|
130
153
|
server.setRequestHandler(GetPromptRequestSchema, async (req) => this.getPrompt(req.params.name, req.params.arguments));
|
|
131
154
|
}
|
|
155
|
+
this.server = server;
|
|
132
156
|
return server;
|
|
133
157
|
}
|
|
134
158
|
/** Every upstream's items from one list method, following cursors. Upstreams that fail are skipped with a warning. */
|
|
@@ -152,6 +176,44 @@ export class AdtlGateway {
|
|
|
152
176
|
}
|
|
153
177
|
return out;
|
|
154
178
|
}
|
|
179
|
+
/** tools/list as the caller sees it: without the tools the authorizer would deny. */
|
|
180
|
+
async visibleTools() {
|
|
181
|
+
const tools = await this.listTools();
|
|
182
|
+
return this.hideDenied(tools, (t) => {
|
|
183
|
+
const r = this.routes.get(t.name);
|
|
184
|
+
return { event: "tools/call", server: r.upstream, tool: r.tool };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** prompts/list as the caller sees it. */
|
|
188
|
+
async visiblePrompts() {
|
|
189
|
+
const prompts = await this.listPrompts();
|
|
190
|
+
return this.hideDenied(prompts, (p) => {
|
|
191
|
+
const r = this.promptRoutes.get(p.name);
|
|
192
|
+
return { event: "prompts/get", server: r.upstream, prompt: r.prompt };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** resources/list as the caller sees it. A URI several upstreams offer is listed as before (reading it is refused). */
|
|
196
|
+
async visibleResources() {
|
|
197
|
+
const resources = await this.listResources();
|
|
198
|
+
return this.hideDenied(resources, (r) => {
|
|
199
|
+
const ups = this.resourceRoutes.get(r.uri);
|
|
200
|
+
return ups?.size === 1 ? { event: "resources/read", server: [...ups][0], resource: r.uri } : undefined;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Leave out items the authorizer would deny, asked without arguments. Not in
|
|
205
|
+
* audit mode, which changes nothing. Hiding only saves the agent a denied
|
|
206
|
+
* call: routes are kept, and a call to a hidden item is still authorized.
|
|
207
|
+
*/
|
|
208
|
+
async hideDenied(items, request) {
|
|
209
|
+
if (!this.authorizers.length || this.invoker.modeOverride === "audit")
|
|
210
|
+
return items;
|
|
211
|
+
const shown = await Promise.all(items.map(async (item) => {
|
|
212
|
+
const r = request(item);
|
|
213
|
+
return r === undefined || (await this.authorize(r)) === undefined;
|
|
214
|
+
}));
|
|
215
|
+
return items.filter((_, i) => shown[i]);
|
|
216
|
+
}
|
|
155
217
|
async listTools() {
|
|
156
218
|
const all = await this.listAll("tools/list", "tools", async (c, cursor) => {
|
|
157
219
|
const p = await c.listTools(cursor ? { cursor } : {});
|
|
@@ -266,6 +328,7 @@ export class AdtlGateway {
|
|
|
266
328
|
const config = { server: upstream };
|
|
267
329
|
const audit = this.invoker.modeOverride === "audit" ? { mode: "audit" } : {};
|
|
268
330
|
let denied;
|
|
331
|
+
const ran = [];
|
|
269
332
|
const finish = (entry) => this.audit?.write({
|
|
270
333
|
ts: context.timestamp,
|
|
271
334
|
sessionId: this.sessionId,
|
|
@@ -274,6 +337,7 @@ export class AdtlGateway {
|
|
|
274
337
|
...target,
|
|
275
338
|
...(denied ? { denied } : {}),
|
|
276
339
|
...entry,
|
|
340
|
+
...(ran.length ? { interceptors: [...ran] } : {}),
|
|
277
341
|
durationMs: Math.round(performance.now() - started),
|
|
278
342
|
});
|
|
279
343
|
const what = "resource" in target ? `resource ${target.resource}` : `prompt ${upstream}${TOOL_SEPARATOR}${target.prompt}`;
|
|
@@ -291,6 +355,7 @@ export class AdtlGateway {
|
|
|
291
355
|
if (denied)
|
|
292
356
|
this.log.info(`audit: would deny ${what}: ${denied.reason}`);
|
|
293
357
|
const req = await this.invoker.runRequest({ event, payload: { method: event, params }, config, context });
|
|
358
|
+
ran.push(...req.ran);
|
|
294
359
|
if (req.blocked) {
|
|
295
360
|
finish({ outcome: "blocked", decision: req.blocked.decision, reason: req.blocked.reason, warnings: req.warnings });
|
|
296
361
|
this.log.info(`${req.blocked.decision} ${what}: ${req.blocked.reason}`);
|
|
@@ -306,6 +371,7 @@ export class AdtlGateway {
|
|
|
306
371
|
throw err;
|
|
307
372
|
}
|
|
308
373
|
const res = await this.invoker.runResponse({ event, payload: { method: event, params: forwarded, result }, config, context });
|
|
374
|
+
ran.push(...res.ran);
|
|
309
375
|
const warnings = [...req.warnings, ...res.warnings];
|
|
310
376
|
if (res.blocked) {
|
|
311
377
|
// Fail closed: unlabeled content must not reach the agent.
|
|
@@ -340,7 +406,15 @@ export class AdtlGateway {
|
|
|
340
406
|
};
|
|
341
407
|
const base = { sessionId: this.sessionId, event: "tools/call", server: route.upstream, tool: route.tool };
|
|
342
408
|
let denied;
|
|
343
|
-
const
|
|
409
|
+
const ran = [];
|
|
410
|
+
const finish = (entry) => this.audit?.write({
|
|
411
|
+
ts: context.timestamp,
|
|
412
|
+
...base,
|
|
413
|
+
...(denied ? { denied } : {}),
|
|
414
|
+
...entry,
|
|
415
|
+
...(ran.length ? { interceptors: [...ran] } : {}),
|
|
416
|
+
durationMs: Math.round(performance.now() - started),
|
|
417
|
+
});
|
|
344
418
|
// 0. Authorization: may this caller use this tool at all? A denial wins over any policy decision.
|
|
345
419
|
denied = await this.authorize({ event: "tools/call", server: route.upstream, tool: route.tool, arguments: args });
|
|
346
420
|
if (denied && this.invoker.modeOverride !== "audit") {
|
|
@@ -358,6 +432,7 @@ export class AdtlGateway {
|
|
|
358
432
|
// 1. Request phase: adtl.policy decides whether the call may run.
|
|
359
433
|
const requestPayload = { method: "tools/call", params: { name: route.tool, arguments: args } };
|
|
360
434
|
let req = await this.invoker.runRequest({ event: "tools/call", payload: requestPayload, config, context });
|
|
435
|
+
ran.push(...req.ran);
|
|
361
436
|
const policy = infoOf(req, "adtl.policy");
|
|
362
437
|
let approval;
|
|
363
438
|
if (req.blocked) {
|
|
@@ -367,8 +442,12 @@ export class AdtlGateway {
|
|
|
367
442
|
const stopped = { decision: b.decision, reason: b.reason, matched: policy.matched, callHash: policy.callHash, sessionLabel: policy.sessionLabel };
|
|
368
443
|
const matched = Array.isArray(policy.matched) ? policy.matched : [];
|
|
369
444
|
const hash = b.confirmation?.callHash ?? policy.callHash;
|
|
370
|
-
|
|
371
|
-
|
|
445
|
+
const lists = Array.isArray(policy.approvers) ? policy.approvers : undefined;
|
|
446
|
+
const approvers = lists ? eligibleApprovers(lists, this.approverRoles) : undefined;
|
|
447
|
+
// The client's own prompt, if enabled and supported; never for named approvers (it can't say who answered).
|
|
448
|
+
const elicit = this.elicitApprovals && !approvers && this.server && supportsElicitation(this.server) ? [new ElicitationChannel(this.server)] : [];
|
|
449
|
+
if (b.decision === "ask" && this.approvals && (this.approvals.canApprove || elicit.length > 0) && typeof hash === "string") {
|
|
450
|
+
this.log.info(`ask ${name}: waiting for approval${approvers ? ` from ${approvers.join(", ") || "nobody"}` : ""} (${b.reason})`);
|
|
372
451
|
approval = await this.waitForApproval(opts, () => this.approvals.request({
|
|
373
452
|
sessionId: this.sessionId,
|
|
374
453
|
server: route.upstream,
|
|
@@ -377,7 +456,8 @@ export class AdtlGateway {
|
|
|
377
456
|
reason: b.reason,
|
|
378
457
|
matched,
|
|
379
458
|
...(policy.sessionLabel !== undefined ? { sessionLabel: policy.sessionLabel } : {}),
|
|
380
|
-
|
|
459
|
+
...(approvers ? { approvers } : {}),
|
|
460
|
+
}, elicit));
|
|
381
461
|
this.log.info(`ask ${name}: ${approval.outcome}`);
|
|
382
462
|
if (approval.outcome !== "approved") {
|
|
383
463
|
finish({ outcome: "blocked", ...stopped, approval, warnings: req.warnings });
|
|
@@ -395,6 +475,7 @@ export class AdtlGateway {
|
|
|
395
475
|
...(approval.approver ? { principal: { type: "user", id: approval.approver } } : {}),
|
|
396
476
|
}];
|
|
397
477
|
req = await this.invoker.runRequest({ event: "tools/call", payload: requestPayload, config, context: { ...context, confirmations } });
|
|
478
|
+
ran.push(...req.ran);
|
|
398
479
|
const sent = callBindingHash({ event: "tools/call", phase: "request", target: route.upstream, payload: req.payload });
|
|
399
480
|
const recheck = req.blocked?.reason ?? (sent !== c.callHash ? "the call changed after it was approved" : undefined);
|
|
400
481
|
if (recheck !== undefined) {
|
|
@@ -438,6 +519,7 @@ export class AdtlGateway {
|
|
|
438
519
|
config,
|
|
439
520
|
context,
|
|
440
521
|
});
|
|
522
|
+
ran.push(...res.ran);
|
|
441
523
|
if (res.blocked) {
|
|
442
524
|
// Fail closed: an unlabeled result must not reach the agent.
|
|
443
525
|
finish({ outcome: "blocked", decision: res.blocked.decision, reason: res.blocked.reason, callHash: policy.callHash, warnings: [...req.warnings, ...res.warnings] });
|