llm-cli-gateway 2.13.2 → 2.14.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 (79) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +68 -29
  3. package/dist/acp/client.d.ts +29 -1
  4. package/dist/acp/client.js +78 -4
  5. package/dist/acp/errors.d.ts +9 -1
  6. package/dist/acp/errors.js +19 -0
  7. package/dist/acp/event-normalizer.d.ts +12 -0
  8. package/dist/acp/event-normalizer.js +16 -0
  9. package/dist/acp/flight-redaction.d.ts +3 -0
  10. package/dist/acp/flight-redaction.js +3 -0
  11. package/dist/acp/permission-bridge.js +11 -5
  12. package/dist/acp/process-manager.d.ts +2 -1
  13. package/dist/acp/process-manager.js +43 -4
  14. package/dist/acp/provider-registry.js +19 -11
  15. package/dist/acp/runtime.d.ts +8 -0
  16. package/dist/acp/runtime.js +47 -4
  17. package/dist/acp/types.d.ts +3083 -55
  18. package/dist/acp/types.js +242 -5
  19. package/dist/async-job-manager.d.ts +38 -1
  20. package/dist/async-job-manager.js +287 -20
  21. package/dist/codex-json-parser.d.ts +1 -0
  22. package/dist/codex-json-parser.js +6 -0
  23. package/dist/config.d.ts +19 -0
  24. package/dist/config.js +84 -1
  25. package/dist/flight-recorder.d.ts +2 -0
  26. package/dist/flight-recorder.js +20 -0
  27. package/dist/gemini-json-parser.d.ts +2 -0
  28. package/dist/gemini-json-parser.js +45 -8
  29. package/dist/grok-json-parser.d.ts +14 -0
  30. package/dist/grok-json-parser.js +156 -0
  31. package/dist/index.d.ts +47 -2
  32. package/dist/index.js +504 -122
  33. package/dist/job-store.d.ts +119 -11
  34. package/dist/job-store.js +372 -42
  35. package/dist/model-registry.d.ts +1 -0
  36. package/dist/model-registry.js +46 -0
  37. package/dist/oauth.js +2 -2
  38. package/dist/postgres-job-store-worker.d.ts +1 -0
  39. package/dist/postgres-job-store-worker.js +444 -0
  40. package/dist/pricing.d.ts +3 -2
  41. package/dist/provider-acp-capabilities.d.ts +52 -0
  42. package/dist/provider-acp-capabilities.js +101 -0
  43. package/dist/provider-admin-tools.d.ts +100 -0
  44. package/dist/provider-admin-tools.js +572 -0
  45. package/dist/provider-capability-cache.d.ts +46 -0
  46. package/dist/provider-capability-cache.js +248 -0
  47. package/dist/provider-capability-discovery.d.ts +85 -0
  48. package/dist/provider-capability-discovery.js +461 -0
  49. package/dist/provider-capability-resolver.d.ts +29 -0
  50. package/dist/provider-capability-resolver.js +92 -0
  51. package/dist/provider-definition-assertions.d.ts +9 -0
  52. package/dist/provider-definition-assertions.js +147 -0
  53. package/dist/provider-definitions.d.ts +127 -0
  54. package/dist/provider-definitions.js +758 -0
  55. package/dist/provider-help-parser.d.ts +34 -0
  56. package/dist/provider-help-parser.js +203 -0
  57. package/dist/provider-model-discovery.d.ts +30 -0
  58. package/dist/provider-model-discovery.js +229 -0
  59. package/dist/provider-output-metadata.d.ts +7 -0
  60. package/dist/provider-output-metadata.js +68 -0
  61. package/dist/provider-schema-builder.d.ts +22 -0
  62. package/dist/provider-schema-builder.js +55 -0
  63. package/dist/provider-surface-generator.d.ts +98 -0
  64. package/dist/provider-surface-generator.js +140 -0
  65. package/dist/provider-tool-capabilities.d.ts +3 -2
  66. package/dist/provider-tool-capabilities.js +77 -62
  67. package/dist/request-helpers.d.ts +37 -4
  68. package/dist/request-helpers.js +134 -0
  69. package/dist/resources.d.ts +6 -1
  70. package/dist/resources.js +64 -192
  71. package/dist/session-manager.js +18 -11
  72. package/dist/sqlite-driver.d.ts +1 -1
  73. package/dist/sqlite-driver.js +2 -1
  74. package/dist/stream-json-parser.d.ts +1 -0
  75. package/dist/stream-json-parser.js +3 -0
  76. package/dist/upstream-contracts.d.ts +17 -1
  77. package/dist/upstream-contracts.js +209 -36
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +8 -3
@@ -0,0 +1,572 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { z } from "zod";
5
+ import { executeCli } from "./executor.js";
6
+ import { noopLogger } from "./logger.js";
7
+ import { redactSecrets } from "./secret-redaction.js";
8
+ import { redactAcpMessage } from "./acp/errors.js";
9
+ import { adminSurfaceKind, getAllProviderDefinitions, getProviderDefinition, } from "./provider-definitions.js";
10
+ import { peekProviderCapabilitySet, resolveProviderCapabilitySet, } from "./provider-capability-resolver.js";
11
+ import { getCliSubcommandContract, } from "./upstream-contracts.js";
12
+ import { getRequestContext, resolveOwnerPrincipal, } from "./request-context.js";
13
+ export function adminRiskToExposure(risk) {
14
+ switch (risk) {
15
+ case "read_only":
16
+ return "mcp_readonly";
17
+ case "writes_local_config":
18
+ case "auth":
19
+ case "network":
20
+ case "destructive":
21
+ return "mcp_requires_approval";
22
+ case "starts_server":
23
+ case "updates_binary":
24
+ case "executes_agent":
25
+ return "not_exposed";
26
+ default: {
27
+ const never = risk;
28
+ return never;
29
+ }
30
+ }
31
+ }
32
+ const READ_VERBS = new Set([
33
+ "list",
34
+ "ls",
35
+ "get",
36
+ "show",
37
+ "status",
38
+ "view",
39
+ "info",
40
+ "doctor",
41
+ "about",
42
+ "version",
43
+ "help",
44
+ "models",
45
+ "model",
46
+ "print",
47
+ "describe",
48
+ "cat",
49
+ "export",
50
+ "paths",
51
+ "path",
52
+ "whoami",
53
+ "current",
54
+ "inspect",
55
+ "trace",
56
+ "changelog",
57
+ "completion",
58
+ "debug",
59
+ ]);
60
+ const AUTH_VERBS = new Set(["login", "logout"]);
61
+ const DESTRUCTIVE_VERBS = new Set([
62
+ "delete",
63
+ "remove",
64
+ "rm",
65
+ "purge",
66
+ "destroy",
67
+ "wipe",
68
+ "reset",
69
+ "clear",
70
+ "uninstall",
71
+ ]);
72
+ const STARTS_SERVER_VERBS = new Set([
73
+ "serve",
74
+ "server",
75
+ "listen",
76
+ "daemon",
77
+ "mcp-server",
78
+ "app-server",
79
+ ]);
80
+ const CONFIG_MUTATE_VERBS = new Set([
81
+ "add",
82
+ "set",
83
+ "enable",
84
+ "disable",
85
+ "install",
86
+ "import",
87
+ "archive",
88
+ "fork",
89
+ "marketplace",
90
+ "register",
91
+ "unregister",
92
+ "edit",
93
+ "create",
94
+ "init",
95
+ "save",
96
+ "apply",
97
+ "sync",
98
+ "start",
99
+ "run",
100
+ "exec",
101
+ ]);
102
+ export function classifyOperationRisk(operationName, familyRisk, opts = {}) {
103
+ const n = operationName.trim().toLowerCase();
104
+ if (READ_VERBS.has(n))
105
+ return "read_only";
106
+ if (AUTH_VERBS.has(n))
107
+ return "auth";
108
+ if (DESTRUCTIVE_VERBS.has(n))
109
+ return "destructive";
110
+ if (STARTS_SERVER_VERBS.has(n))
111
+ return "starts_server";
112
+ if (CONFIG_MUTATE_VERBS.has(n))
113
+ return "writes_local_config";
114
+ if (opts.isSubcommand && familyRisk === "read_only") {
115
+ return "writes_local_config";
116
+ }
117
+ return familyRisk;
118
+ }
119
+ export function familyBaseRisk(provider, fam) {
120
+ const contract = getCliSubcommandContract(provider, [fam.family]);
121
+ if (contract)
122
+ return contract.risk;
123
+ return fam.safety === "read-only" ? "read_only" : "writes_local_config";
124
+ }
125
+ function rootAdvertises(discovered, family) {
126
+ const wanted = family.toLowerCase();
127
+ return discovered.rootHelp.subcommands.some(s => s.name.toLowerCase() === wanted);
128
+ }
129
+ function familySubHelp(discovered, family) {
130
+ const wanted = family.toLowerCase();
131
+ for (const [key, help] of Object.entries(discovered.subcommandHelp)) {
132
+ const tokens = key.split(/\s+/).filter(Boolean);
133
+ const first = tokens.find(t => !t.startsWith("-"));
134
+ if (first && first.toLowerCase() === wanted)
135
+ return help;
136
+ }
137
+ return null;
138
+ }
139
+ export function projectProviderAdminOperations(def, discovered) {
140
+ const ops = [];
141
+ for (const fam of def.adminSubcommands) {
142
+ if (adminSurfaceKind(fam) !== "cli-subcommand")
143
+ continue;
144
+ const baseRisk = familyBaseRisk(def.id, fam);
145
+ if (!discovered) {
146
+ ops.push({
147
+ provider: def.id,
148
+ family: fam.family,
149
+ operationId: fam.family,
150
+ argv: [fam.family],
151
+ risk: baseRisk,
152
+ exposure: adminRiskToExposure(baseRisk),
153
+ mutating: adminRiskToExposure(baseRisk) === "mcp_requires_approval",
154
+ available: false,
155
+ discoverySource: "no-discovery",
156
+ summary: fam.evidence,
157
+ });
158
+ continue;
159
+ }
160
+ const advertised = rootAdvertises(discovered, fam.family);
161
+ const subHelp = familySubHelp(discovered, fam.family);
162
+ const subOps = subHelp?.subcommands ?? [];
163
+ if (subOps.length > 0) {
164
+ for (const sub of subOps) {
165
+ if (!isSafeAdminToken(sub.name))
166
+ continue;
167
+ const risk = classifyOperationRisk(sub.name, baseRisk, { isSubcommand: true });
168
+ const exposure = adminRiskToExposure(risk);
169
+ ops.push({
170
+ provider: def.id,
171
+ family: fam.family,
172
+ operationId: `${fam.family}.${sub.name}`,
173
+ argv: [fam.family, sub.name],
174
+ risk,
175
+ exposure,
176
+ mutating: exposure === "mcp_requires_approval",
177
+ available: exposure !== "not_exposed" && exposure !== "tracked_only",
178
+ discoverySource: "subcommand-help",
179
+ summary: sub.description || fam.evidence,
180
+ });
181
+ }
182
+ continue;
183
+ }
184
+ const baseExposure = adminRiskToExposure(baseRisk);
185
+ const risk = baseExposure === "not_exposed" ? baseRisk : classifyOperationRisk(fam.family, baseRisk);
186
+ const exposure = adminRiskToExposure(risk);
187
+ ops.push({
188
+ provider: def.id,
189
+ family: fam.family,
190
+ operationId: fam.family,
191
+ argv: [fam.family],
192
+ risk,
193
+ exposure,
194
+ mutating: exposure === "mcp_requires_approval",
195
+ available: advertised && exposure !== "not_exposed" && exposure !== "tracked_only",
196
+ discoverySource: advertised ? "root-help" : "not-advertised",
197
+ summary: fam.evidence,
198
+ });
199
+ }
200
+ return ops;
201
+ }
202
+ export function buildProviderAdminCatalog(options = {}) {
203
+ const lookup = options.lookup ?? ((id) => peekProviderCapabilitySet(id)?.set ?? null);
204
+ const defs = options.provider
205
+ ? [getProviderDefinition(options.provider)]
206
+ : getAllProviderDefinitions();
207
+ const rows = [];
208
+ for (const def of defs) {
209
+ const discovered = lookup(def.id);
210
+ for (const op of projectProviderAdminOperations(def, discovered)) {
211
+ if (!options.includeUnavailable && !op.available)
212
+ continue;
213
+ rows.push({
214
+ provider: op.provider,
215
+ operationId: op.operationId,
216
+ family: op.family,
217
+ argv: op.argv,
218
+ risk: op.risk,
219
+ exposure: op.exposure,
220
+ mutating: op.mutating,
221
+ available: op.available,
222
+ discoverySource: op.discoverySource,
223
+ summary: op.summary.length > 80 ? `${op.summary.slice(0, 77).trimEnd()}...` : op.summary,
224
+ });
225
+ }
226
+ }
227
+ return rows.sort((a, b) => `${a.provider}:${a.operationId}`.localeCompare(`${b.provider}:${b.operationId}`));
228
+ }
229
+ const REDACTED = "[REDACTED]";
230
+ const OAUTH_CODE_RE = /\b(code|verifier|challenge|state|nonce|otp)\s*[:=]\s*[A-Za-z0-9._~+/=-]{6,}/gi;
231
+ const ACCOUNT_ID_RE = /\b(?:user|customer|account|acct|org|tenant)[_-][A-Za-z0-9]{6,}\b/gi;
232
+ export function redactAdminOutput(text) {
233
+ if (!text)
234
+ return text;
235
+ let out = redactSecrets(text);
236
+ out = out.replace(OAUTH_CODE_RE, (_m, key) => `${key}=${REDACTED}`);
237
+ out = out.replace(ACCOUNT_ID_RE, REDACTED);
238
+ out = redactAcpMessage(out);
239
+ return out;
240
+ }
241
+ const defaultAdminRunner = (executable, argv, options) => executeCli(executable, [...argv], { timeout: options.timeoutMs });
242
+ export function isSafeAdminToken(token) {
243
+ if (token.length === 0 || token.length > 64)
244
+ return false;
245
+ return /^-{0,2}[A-Za-z0-9][A-Za-z0-9._-]*$/.test(token);
246
+ }
247
+ function assertSafeArgv(argv) {
248
+ for (const token of argv) {
249
+ if (!isSafeAdminToken(token)) {
250
+ throw new Error(`Unsafe admin argv token rejected: ${JSON.stringify(token)}`);
251
+ }
252
+ }
253
+ }
254
+ export async function runReadOnlyAdminOperation(op, deps = {}) {
255
+ const logger = deps.logger ?? noopLogger;
256
+ if (op.exposure !== "mcp_readonly") {
257
+ return {
258
+ ok: false,
259
+ provider: op.provider,
260
+ operationId: op.operationId,
261
+ argv: op.argv,
262
+ exitCode: null,
263
+ stdout: "",
264
+ stderr: "",
265
+ redacted: true,
266
+ error: `Operation ${op.operationId} is not read-only (exposure=${op.exposure}); use the mutating admin tool.`,
267
+ };
268
+ }
269
+ if (!op.available) {
270
+ return {
271
+ ok: false,
272
+ provider: op.provider,
273
+ operationId: op.operationId,
274
+ argv: op.argv,
275
+ exitCode: null,
276
+ stdout: "",
277
+ stderr: "",
278
+ redacted: true,
279
+ error: `Operation ${op.operationId} is not advertised by the installed ${op.provider} CLI.`,
280
+ };
281
+ }
282
+ assertSafeArgv(op.argv);
283
+ const def = getProviderDefinition(op.provider);
284
+ const runner = deps.runner ?? defaultAdminRunner;
285
+ try {
286
+ const result = await runner(def.primaryExecutable, op.argv, { timeoutMs: deps.timeoutMs });
287
+ return {
288
+ ok: result.code === 0,
289
+ provider: op.provider,
290
+ operationId: op.operationId,
291
+ argv: op.argv,
292
+ exitCode: result.code,
293
+ stdout: redactAdminOutput(result.stdout),
294
+ stderr: redactAdminOutput(result.stderr),
295
+ redacted: true,
296
+ };
297
+ }
298
+ catch (err) {
299
+ const reason = err instanceof Error ? err.message : String(err);
300
+ logger.debug(`admin read-only op failed: ${op.operationId}`, { reason });
301
+ return {
302
+ ok: false,
303
+ provider: op.provider,
304
+ operationId: op.operationId,
305
+ argv: op.argv,
306
+ exitCode: null,
307
+ stdout: "",
308
+ stderr: "",
309
+ redacted: true,
310
+ error: redactAdminOutput(reason),
311
+ };
312
+ }
313
+ }
314
+ export function defaultAdminAuditPath() {
315
+ return join(homedir(), ".llm-cli-gateway", "admin-audit.jsonl");
316
+ }
317
+ export function defaultAdminAuditSink(auditPath) {
318
+ return record => {
319
+ const path = auditPath ?? defaultAdminAuditPath();
320
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
321
+ appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf-8", mode: 0o600 });
322
+ };
323
+ }
324
+ function appendAdminAuditBestEffort(record, sink) {
325
+ try {
326
+ sink(record);
327
+ }
328
+ catch {
329
+ }
330
+ }
331
+ export function appendAdminAudit(record, auditPath) {
332
+ appendAdminAuditBestEffort(record, defaultAdminAuditSink(auditPath));
333
+ }
334
+ export async function runMutatingAdminOperation(op, ctx) {
335
+ const logger = ctx.logger ?? noopLogger;
336
+ const sink = ctx.auditSink ?? defaultAdminAuditSink(ctx.auditPath);
337
+ const base = {
338
+ provider: op.provider,
339
+ operationId: op.operationId,
340
+ argv: op.argv,
341
+ redacted: true,
342
+ };
343
+ const fail = (error) => ({
344
+ ok: false,
345
+ ...base,
346
+ exitCode: null,
347
+ stdout: "",
348
+ stderr: "",
349
+ error,
350
+ });
351
+ const auditBestEffort = (outcome, extra = {}) => appendAdminAuditBestEffort({
352
+ ts: new Date().toISOString(),
353
+ provider: op.provider,
354
+ operationId: op.operationId,
355
+ argv: op.argv,
356
+ outcome,
357
+ principal: ctx.principal,
358
+ ...extra,
359
+ }, sink);
360
+ if (!op.available) {
361
+ return fail(`Operation ${op.operationId} is not advertised by the installed ${op.provider} CLI.`);
362
+ }
363
+ if (op.exposure !== "mcp_requires_approval") {
364
+ return fail(`Operation ${op.operationId} (exposure=${op.exposure}) is not an approval-gated mutating admin op.`);
365
+ }
366
+ if (ctx.remoteCaller && !ctx.remoteAdminAllowed) {
367
+ auditBestEffort("gate_closed");
368
+ return fail(`Mutating admin operation ${op.operationId} is not permitted for remote callers. ` +
369
+ `Set LLM_GATEWAY_CLI_ADMIN=1 and grant the cli:admin OAuth scope to permit it.`);
370
+ }
371
+ if (!ctx.allowMutating) {
372
+ auditBestEffort("gate_closed");
373
+ return fail(`Mutating admin operation ${op.operationId} is disabled. Set [admin] ` +
374
+ `allow_mutating_cli_admin_ops = true in ~/.llm-cli-gateway/config.toml to permit it.`);
375
+ }
376
+ assertSafeArgv(op.argv);
377
+ const decision = ctx.approvalManager.decide({
378
+ cli: op.provider,
379
+ operation: `cli_admin:${op.operationId}`,
380
+ prompt: `Provider CLI admin mutation: ${op.provider} ${op.argv.join(" ")} (${op.risk})`,
381
+ bypassRequested: false,
382
+ fullAuto: false,
383
+ requestedMcpServers: [],
384
+ metadata: { adminOperation: op.operationId, risk: op.risk, argv: op.argv },
385
+ });
386
+ if (decision.status !== "approved") {
387
+ auditBestEffort("denied", { approvalId: decision.id });
388
+ return fail(`Mutating admin operation ${op.operationId} denied by approval policy (${decision.reasons.join("; ")}).`);
389
+ }
390
+ try {
391
+ sink({
392
+ ts: new Date().toISOString(),
393
+ provider: op.provider,
394
+ operationId: op.operationId,
395
+ argv: op.argv,
396
+ outcome: "approved",
397
+ approvalId: decision.id,
398
+ principal: ctx.principal,
399
+ });
400
+ }
401
+ catch (err) {
402
+ const reason = err instanceof Error ? err.message : String(err);
403
+ logger.error(`admin mutating op aborted: audit write failed for ${op.operationId}`, { reason });
404
+ return fail(`Mutating admin operation ${op.operationId} aborted: the approved-intent audit record ` +
405
+ `could not be persisted, so the mutation was not run.`);
406
+ }
407
+ const def = getProviderDefinition(op.provider);
408
+ const runner = ctx.runner ?? defaultAdminRunner;
409
+ try {
410
+ const result = await runner(def.primaryExecutable, op.argv, { timeoutMs: ctx.timeoutMs });
411
+ auditBestEffort("executed", { approvalId: decision.id, exitCode: result.code });
412
+ return {
413
+ ok: result.code === 0,
414
+ ...base,
415
+ exitCode: result.code,
416
+ stdout: redactAdminOutput(result.stdout),
417
+ stderr: redactAdminOutput(result.stderr),
418
+ };
419
+ }
420
+ catch (err) {
421
+ const reason = err instanceof Error ? err.message : String(err);
422
+ logger.debug(`admin mutating op failed: ${op.operationId}`, { reason });
423
+ auditBestEffort("executed", { approvalId: decision.id, exitCode: null });
424
+ return fail(redactAdminOutput(reason));
425
+ }
426
+ }
427
+ export function isRemoteAdminCaller(ctx) {
428
+ return ctx?.transport === "http" || ctx?.authKind === "oauth";
429
+ }
430
+ export function remoteCliAdminEnabled(ctx) {
431
+ const scopes = ctx?.authScopes ?? [];
432
+ return process.env.LLM_GATEWAY_CLI_ADMIN === "1" && scopes.includes("cli:admin");
433
+ }
434
+ async function resolveAdminOperation(provider, operationId, logger) {
435
+ const def = getProviderDefinition(provider);
436
+ let discovered = peekProviderCapabilitySet(provider)?.set ?? null;
437
+ if (!discovered) {
438
+ const resolved = await resolveProviderCapabilitySet(def, { logger });
439
+ discovered = resolved?.set ?? null;
440
+ }
441
+ const ops = projectProviderAdminOperations(def, discovered);
442
+ return ops.find(o => o.operationId === operationId) ?? null;
443
+ }
444
+ const PROVIDER_ADMIN_ENUM = z.enum(getAllProviderDefinitions().map(d => d.id));
445
+ function remoteReadOnlyAdminGate(toolName) {
446
+ const ctx = getRequestContext();
447
+ if (isRemoteAdminCaller(ctx) && !remoteCliAdminEnabled(ctx)) {
448
+ return {
449
+ content: [
450
+ {
451
+ type: "text",
452
+ text: JSON.stringify({
453
+ ok: false,
454
+ error: `${toolName} is not permitted for remote callers. Set LLM_GATEWAY_CLI_ADMIN=1 ` +
455
+ `and grant the cli:admin OAuth scope to permit read-only provider admin operations remotely.`,
456
+ }),
457
+ },
458
+ ],
459
+ };
460
+ }
461
+ return null;
462
+ }
463
+ export function registerProviderAdminTools(server, runtime) {
464
+ server.tool("provider_admin_list", "List provider CLI admin operations (auth status, model list, mcp list, plugin list, doctor, etc.) available on the installed CLIs, projected from runtime discovery. Read-only.", {
465
+ provider: z
466
+ .preprocess(value => (value === "" || value === null ? undefined : value), PROVIDER_ADMIN_ENUM.optional())
467
+ .describe("Optional provider filter"),
468
+ includeUnavailable: z
469
+ .boolean()
470
+ .default(false)
471
+ .describe("Include operations the installed CLI does not advertise or the policy hides"),
472
+ }, {
473
+ title: "Provider admin operations catalog",
474
+ readOnlyHint: true,
475
+ destructiveHint: false,
476
+ idempotentHint: true,
477
+ openWorldHint: false,
478
+ }, async ({ provider, includeUnavailable }) => {
479
+ const gate = remoteReadOnlyAdminGate("provider_admin_list");
480
+ if (gate)
481
+ return gate;
482
+ const targets = provider ? [getProviderDefinition(provider)] : getAllProviderDefinitions();
483
+ await Promise.all(targets.map(def => peekProviderCapabilitySet(def.id)
484
+ ? Promise.resolve(null)
485
+ : resolveProviderCapabilitySet(def, { logger: runtime.logger }).catch(() => null)));
486
+ const rows = buildProviderAdminCatalog({ provider, includeUnavailable });
487
+ return {
488
+ content: [
489
+ {
490
+ type: "text",
491
+ text: JSON.stringify({
492
+ schemaVersion: "provider-admin-catalog.v1",
493
+ mutatingEnabled: runtime.allowMutatingCliAdminOps,
494
+ total: rows.length,
495
+ rows,
496
+ }),
497
+ },
498
+ ],
499
+ };
500
+ });
501
+ server.tool("provider_admin_run", "Execute a READ-ONLY provider CLI admin operation (from provider_admin_list) and return redacted output. Rejects mutating operations.", {
502
+ provider: PROVIDER_ADMIN_ENUM.describe("Provider whose admin operation to run"),
503
+ operationId: z
504
+ .string()
505
+ .min(1)
506
+ .describe("Operation id from provider_admin_list, e.g. 'mcp.list' or 'doctor'"),
507
+ }, {
508
+ title: "Run read-only provider admin operation",
509
+ readOnlyHint: true,
510
+ destructiveHint: false,
511
+ idempotentHint: true,
512
+ openWorldHint: false,
513
+ }, async ({ provider, operationId }) => {
514
+ const gate = remoteReadOnlyAdminGate("provider_admin_run");
515
+ if (gate)
516
+ return gate;
517
+ const op = await resolveAdminOperation(provider, operationId, runtime.logger);
518
+ if (!op) {
519
+ return {
520
+ content: [
521
+ {
522
+ type: "text",
523
+ text: JSON.stringify({
524
+ ok: false,
525
+ error: `No admin operation '${operationId}' for ${provider} (not advertised by the installed CLI).`,
526
+ }),
527
+ },
528
+ ],
529
+ };
530
+ }
531
+ const result = await runReadOnlyAdminOperation(op, { logger: runtime.logger });
532
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
533
+ });
534
+ server.tool("provider_admin_mutate", "Execute a MUTATING provider CLI admin operation (mcp add/remove, login/logout, plugin install/remove, session delete/archive, ...). Disabled unless [admin] allow_mutating_cli_admin_ops=true; routed through approval and audited.", {
535
+ provider: PROVIDER_ADMIN_ENUM.describe("Provider whose admin operation to run"),
536
+ operationId: z
537
+ .string()
538
+ .min(1)
539
+ .describe("Mutating operation id from provider_admin_list, e.g. 'mcp.remove'"),
540
+ }, {
541
+ title: "Run mutating provider admin operation (gated)",
542
+ readOnlyHint: false,
543
+ destructiveHint: true,
544
+ idempotentHint: false,
545
+ openWorldHint: false,
546
+ }, async ({ provider, operationId }) => {
547
+ const op = await resolveAdminOperation(provider, operationId, runtime.logger);
548
+ if (!op) {
549
+ return {
550
+ content: [
551
+ {
552
+ type: "text",
553
+ text: JSON.stringify({
554
+ ok: false,
555
+ error: `No admin operation '${operationId}' for ${provider} (not advertised by the installed CLI).`,
556
+ }),
557
+ },
558
+ ],
559
+ };
560
+ }
561
+ const reqCtx = getRequestContext();
562
+ const result = await runMutatingAdminOperation(op, {
563
+ allowMutating: runtime.allowMutatingCliAdminOps,
564
+ remoteCaller: isRemoteAdminCaller(reqCtx),
565
+ remoteAdminAllowed: remoteCliAdminEnabled(reqCtx),
566
+ principal: resolveOwnerPrincipal(reqCtx),
567
+ approvalManager: runtime.approvalManager,
568
+ logger: runtime.logger,
569
+ });
570
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
571
+ });
572
+ }
@@ -0,0 +1,46 @@
1
+ import type { CliType } from "./provider-definitions.js";
2
+ import type { DiscoveredCapabilitySet } from "./provider-capability-discovery.js";
3
+ export declare const CAPABILITY_CACHE_SCHEMA_VERSION = "provider-capability-cache.v1";
4
+ export interface CapabilityCacheKeyFields {
5
+ readonly providerId: CliType;
6
+ readonly executablePath: string;
7
+ readonly version: string;
8
+ readonly rootHelpChecksum: string;
9
+ readonly subcommandHelpChecksums: Readonly<Record<string, string>>;
10
+ readonly acpInitializeChecksum: string | null;
11
+ readonly modelCatalogChecksum: string;
12
+ readonly gatewayVersion: string;
13
+ }
14
+ export interface CachedCapabilityEntry {
15
+ readonly schemaVersion: typeof CAPABILITY_CACHE_SCHEMA_VERSION;
16
+ readonly providerId: CliType;
17
+ readonly cacheKey: string;
18
+ readonly keyFields: CapabilityCacheKeyFields;
19
+ readonly capabilitySet: DiscoveredCapabilitySet;
20
+ readonly cachedAt: string;
21
+ readonly source: "discovery";
22
+ }
23
+ export interface CapabilityCacheLookup {
24
+ readonly hit: boolean;
25
+ readonly ageMs: number | null;
26
+ readonly cachedAt: string | null;
27
+ readonly source: string | null;
28
+ readonly version: string | null;
29
+ readonly checksum: string | null;
30
+ readonly entry: CachedCapabilityEntry | null;
31
+ }
32
+ export declare function capabilityCacheDir(): string;
33
+ export declare function cacheKeyFields(set: DiscoveredCapabilitySet): CapabilityCacheKeyFields;
34
+ export declare function computeCacheKey(set: DiscoveredCapabilitySet): string;
35
+ export declare function scrubString(value: string): string;
36
+ export declare function scrubSecrets<T>(value: T): T;
37
+ export declare function readCapabilityCache(providerId: CliType): CachedCapabilityEntry | null;
38
+ export declare function writeCapabilityCache(set: DiscoveredCapabilitySet): CachedCapabilityEntry;
39
+ export declare function lookupCapabilityCache(freshSet: DiscoveredCapabilitySet): CapabilityCacheLookup;
40
+ export interface ResolvedCapability {
41
+ readonly set: DiscoveredCapabilitySet;
42
+ readonly source: "discovery" | "cache" | "minimal";
43
+ readonly degraded: boolean;
44
+ readonly reason?: string;
45
+ }
46
+ export declare function resolveCapabilitySet(freshSet: DiscoveredCapabilitySet): ResolvedCapability;