llm-cli-gateway 2.13.1 → 2.14.0-rc.1

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 (88) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +53 -26
  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 +2 -0
  20. package/dist/async-job-manager.js +11 -0
  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 +16 -0
  24. package/dist/config.js +105 -19
  25. package/dist/connector-setup.d.ts +39 -0
  26. package/dist/connector-setup.js +86 -0
  27. package/dist/doctor.d.ts +39 -1
  28. package/dist/doctor.js +182 -3
  29. package/dist/flight-recorder.d.ts +2 -0
  30. package/dist/flight-recorder.js +20 -0
  31. package/dist/gemini-json-parser.d.ts +2 -0
  32. package/dist/gemini-json-parser.js +45 -8
  33. package/dist/grok-json-parser.d.ts +14 -0
  34. package/dist/grok-json-parser.js +156 -0
  35. package/dist/http-transport.js +27 -3
  36. package/dist/index.d.ts +48 -2
  37. package/dist/index.js +633 -144
  38. package/dist/job-store.d.ts +45 -7
  39. package/dist/job-store.js +138 -17
  40. package/dist/model-registry.d.ts +1 -0
  41. package/dist/model-registry.js +46 -0
  42. package/dist/oauth.js +17 -16
  43. package/dist/postgres-job-store-worker.d.ts +1 -0
  44. package/dist/postgres-job-store-worker.js +327 -0
  45. package/dist/pricing.d.ts +3 -2
  46. package/dist/provider-acp-capabilities.d.ts +52 -0
  47. package/dist/provider-acp-capabilities.js +101 -0
  48. package/dist/provider-admin-tools.d.ts +100 -0
  49. package/dist/provider-admin-tools.js +572 -0
  50. package/dist/provider-capability-cache.d.ts +46 -0
  51. package/dist/provider-capability-cache.js +248 -0
  52. package/dist/provider-capability-discovery.d.ts +85 -0
  53. package/dist/provider-capability-discovery.js +461 -0
  54. package/dist/provider-capability-resolver.d.ts +29 -0
  55. package/dist/provider-capability-resolver.js +92 -0
  56. package/dist/provider-definition-assertions.d.ts +9 -0
  57. package/dist/provider-definition-assertions.js +147 -0
  58. package/dist/provider-definitions.d.ts +127 -0
  59. package/dist/provider-definitions.js +758 -0
  60. package/dist/provider-help-parser.d.ts +34 -0
  61. package/dist/provider-help-parser.js +203 -0
  62. package/dist/provider-model-discovery.d.ts +30 -0
  63. package/dist/provider-model-discovery.js +229 -0
  64. package/dist/provider-output-metadata.d.ts +7 -0
  65. package/dist/provider-output-metadata.js +68 -0
  66. package/dist/provider-schema-builder.d.ts +22 -0
  67. package/dist/provider-schema-builder.js +55 -0
  68. package/dist/provider-surface-generator.d.ts +98 -0
  69. package/dist/provider-surface-generator.js +140 -0
  70. package/dist/provider-tool-capabilities.d.ts +3 -2
  71. package/dist/provider-tool-capabilities.js +77 -62
  72. package/dist/remote-url.d.ts +28 -0
  73. package/dist/remote-url.js +61 -0
  74. package/dist/request-helpers.d.ts +37 -4
  75. package/dist/request-helpers.js +134 -0
  76. package/dist/resources.d.ts +6 -1
  77. package/dist/resources.js +67 -193
  78. package/dist/session-manager.d.ts +2 -0
  79. package/dist/session-manager.js +34 -8
  80. package/dist/stream-json-parser.d.ts +1 -0
  81. package/dist/stream-json-parser.js +3 -0
  82. package/dist/upstream-contracts.d.ts +17 -1
  83. package/dist/upstream-contracts.js +209 -36
  84. package/dist/workspace-registry.d.ts +9 -0
  85. package/dist/workspace-registry.js +24 -4
  86. package/npm-shrinkwrap.json +2 -2
  87. package/package.json +8 -3
  88. package/setup/status.schema.json +75 -0
package/dist/acp/types.js CHANGED
@@ -57,10 +57,60 @@ export const InitializeRequestSchema = z
57
57
  _meta: MetaSchema,
58
58
  })
59
59
  .passthrough();
60
+ const SessionSubCapabilitySchema = z.object({ _meta: MetaSchema }).passthrough().nullish();
61
+ export const PromptCapabilitiesSchema = z
62
+ .object({
63
+ image: z.boolean().optional(),
64
+ audio: z.boolean().optional(),
65
+ embeddedContext: z.boolean().optional(),
66
+ _meta: MetaSchema,
67
+ })
68
+ .passthrough();
69
+ export const McpCapabilitiesSchema = z
70
+ .object({
71
+ http: z.boolean().optional(),
72
+ sse: z.boolean().optional(),
73
+ _meta: MetaSchema,
74
+ })
75
+ .passthrough();
76
+ export const SessionCapabilitiesSchema = z
77
+ .object({
78
+ resume: SessionSubCapabilitySchema,
79
+ list: SessionSubCapabilitySchema,
80
+ close: SessionSubCapabilitySchema,
81
+ delete: SessionSubCapabilitySchema,
82
+ additionalDirectories: SessionSubCapabilitySchema,
83
+ _meta: MetaSchema,
84
+ })
85
+ .passthrough();
86
+ export const AgentAuthCapabilitiesSchema = z
87
+ .object({
88
+ logout: z.object({ _meta: MetaSchema }).passthrough().nullish(),
89
+ _meta: MetaSchema,
90
+ })
91
+ .passthrough();
92
+ export const AgentCapabilitiesSchema = z
93
+ .object({
94
+ loadSession: z.boolean().optional(),
95
+ promptCapabilities: PromptCapabilitiesSchema.optional(),
96
+ mcpCapabilities: McpCapabilitiesSchema.optional(),
97
+ sessionCapabilities: SessionCapabilitiesSchema.optional(),
98
+ auth: AgentAuthCapabilitiesSchema.optional(),
99
+ _meta: MetaSchema,
100
+ })
101
+ .passthrough();
102
+ export const AuthMethodSchema = z
103
+ .object({
104
+ id: z.string().optional(),
105
+ name: z.string().optional(),
106
+ description: z.string().nullish(),
107
+ _meta: MetaSchema,
108
+ })
109
+ .passthrough();
60
110
  export const InitializeResponseSchema = z
61
111
  .object({
62
112
  protocolVersion: ProtocolVersionSchema,
63
- agentCapabilities: z.record(z.unknown()).optional(),
113
+ agentCapabilities: AgentCapabilitiesSchema.optional(),
64
114
  agentInfo: z
65
115
  .object({
66
116
  name: z.string().optional(),
@@ -68,10 +118,51 @@ export const InitializeResponseSchema = z
68
118
  })
69
119
  .passthrough()
70
120
  .optional(),
71
- authMethods: z.array(z.record(z.unknown())).optional(),
121
+ authMethods: z.array(AuthMethodSchema).optional(),
72
122
  _meta: MetaSchema,
73
123
  })
74
124
  .passthrough();
125
+ export const BASELINE_ACP_METHODS = [
126
+ "session/new",
127
+ "session/prompt",
128
+ "session/cancel",
129
+ "session/update",
130
+ ];
131
+ function subCapabilityPresent(value) {
132
+ return value !== null && value !== undefined && typeof value === "object";
133
+ }
134
+ export function deriveAcpMethodAvailability(init) {
135
+ const methods = new Set(BASELINE_ACP_METHODS);
136
+ const caps = init.agentCapabilities;
137
+ if (caps?.loadSession === true) {
138
+ methods.add("session/load");
139
+ }
140
+ const sc = caps?.sessionCapabilities;
141
+ if (sc) {
142
+ if (subCapabilityPresent(sc.resume))
143
+ methods.add("session/resume");
144
+ if (subCapabilityPresent(sc.list))
145
+ methods.add("session/list");
146
+ if (subCapabilityPresent(sc.close))
147
+ methods.add("session/close");
148
+ if (subCapabilityPresent(sc.delete))
149
+ methods.add("session/delete");
150
+ }
151
+ if (Array.isArray(init.authMethods) && init.authMethods.length > 0) {
152
+ methods.add("authenticate");
153
+ }
154
+ return methods;
155
+ }
156
+ export function sessionResponseMethods(response) {
157
+ const methods = new Set();
158
+ if (response.modes !== null && response.modes !== undefined) {
159
+ methods.add("session/set_mode");
160
+ }
161
+ if (response.configOptions !== null && response.configOptions !== undefined) {
162
+ methods.add("session/set_config_option");
163
+ }
164
+ return methods;
165
+ }
75
166
  export const McpServerSchema = z.record(z.unknown());
76
167
  export const SessionNewRequestSchema = z
77
168
  .object({
@@ -83,7 +174,8 @@ export const SessionNewRequestSchema = z
83
174
  export const SessionNewResponseSchema = z
84
175
  .object({
85
176
  sessionId: SessionIdSchema,
86
- modes: z.record(z.unknown()).optional(),
177
+ modes: z.record(z.unknown()).nullish(),
178
+ configOptions: z.array(z.record(z.unknown())).nullish(),
87
179
  _meta: MetaSchema,
88
180
  })
89
181
  .passthrough();
@@ -97,7 +189,124 @@ export const SessionLoadRequestSchema = z
97
189
  .passthrough();
98
190
  export const SessionLoadResponseSchema = z
99
191
  .object({
100
- modes: z.record(z.unknown()).optional(),
192
+ modes: z.record(z.unknown()).nullish(),
193
+ configOptions: z.array(z.record(z.unknown())).nullish(),
194
+ _meta: MetaSchema,
195
+ })
196
+ .passthrough();
197
+ export const SessionResumeRequestSchema = z
198
+ .object({
199
+ sessionId: SessionIdSchema,
200
+ cwd: z.string().min(1),
201
+ mcpServers: z.array(McpServerSchema).default([]),
202
+ _meta: MetaSchema,
203
+ })
204
+ .passthrough();
205
+ export const SessionResumeResponseSchema = z
206
+ .object({
207
+ modes: z.record(z.unknown()).nullish(),
208
+ configOptions: z.array(z.record(z.unknown())).nullish(),
209
+ _meta: MetaSchema,
210
+ })
211
+ .passthrough();
212
+ export const SessionModeSchema = z
213
+ .object({
214
+ id: z.string().min(1),
215
+ name: z.string().min(1),
216
+ description: z.string().nullish(),
217
+ _meta: MetaSchema,
218
+ })
219
+ .passthrough();
220
+ export const SessionModeStateSchema = z
221
+ .object({
222
+ currentModeId: z.string().min(1),
223
+ availableModes: z.array(SessionModeSchema),
224
+ _meta: MetaSchema,
225
+ })
226
+ .passthrough();
227
+ export const SessionConfigSelectValueSchema = z
228
+ .object({
229
+ value: z.string().min(1),
230
+ label: z.string().nullish(),
231
+ description: z.string().nullish(),
232
+ _meta: MetaSchema,
233
+ })
234
+ .passthrough();
235
+ export const SessionConfigOptionSchema = z.discriminatedUnion("type", [
236
+ z
237
+ .object({
238
+ type: z.literal("select"),
239
+ id: z.string().min(1),
240
+ name: z.string().min(1),
241
+ description: z.string().nullish(),
242
+ category: z.string().nullish(),
243
+ currentValue: z.string().min(1),
244
+ options: z.array(SessionConfigSelectValueSchema),
245
+ _meta: MetaSchema,
246
+ })
247
+ .passthrough(),
248
+ z
249
+ .object({
250
+ type: z.literal("boolean"),
251
+ id: z.string().min(1),
252
+ name: z.string().min(1),
253
+ description: z.string().nullish(),
254
+ category: z.string().nullish(),
255
+ currentValue: z.boolean(),
256
+ _meta: MetaSchema,
257
+ })
258
+ .passthrough(),
259
+ ]);
260
+ export const SessionInfoSchema = z
261
+ .object({
262
+ sessionId: SessionIdSchema,
263
+ cwd: z.string().min(1),
264
+ additionalDirectories: z.array(z.string()).optional(),
265
+ title: z.string().nullish(),
266
+ updatedAt: z.string().nullish(),
267
+ _meta: MetaSchema,
268
+ })
269
+ .passthrough();
270
+ export const ListSessionsRequestSchema = z
271
+ .object({
272
+ cursor: z.string().nullish(),
273
+ _meta: MetaSchema,
274
+ })
275
+ .passthrough();
276
+ export const ListSessionsResponseSchema = z
277
+ .object({
278
+ sessions: z.array(SessionInfoSchema),
279
+ nextCursor: z.string().nullish(),
280
+ _meta: MetaSchema,
281
+ })
282
+ .passthrough();
283
+ export const CloseSessionRequestSchema = z
284
+ .object({ sessionId: SessionIdSchema, _meta: MetaSchema })
285
+ .passthrough();
286
+ export const CloseSessionResponseSchema = z.object({ _meta: MetaSchema }).passthrough();
287
+ export const DeleteSessionRequestSchema = z
288
+ .object({ sessionId: SessionIdSchema, _meta: MetaSchema })
289
+ .passthrough();
290
+ export const DeleteSessionResponseSchema = z.object({ _meta: MetaSchema }).passthrough();
291
+ export const SetSessionModeRequestSchema = z
292
+ .object({
293
+ sessionId: SessionIdSchema,
294
+ modeId: z.string().min(1),
295
+ _meta: MetaSchema,
296
+ })
297
+ .passthrough();
298
+ export const SetSessionModeResponseSchema = z.object({ _meta: MetaSchema }).passthrough();
299
+ export const SetSessionConfigOptionRequestSchema = z
300
+ .object({
301
+ sessionId: SessionIdSchema,
302
+ configId: z.string().min(1),
303
+ value: z.string().min(1),
304
+ _meta: MetaSchema,
305
+ })
306
+ .passthrough();
307
+ export const SetSessionConfigOptionResponseSchema = z
308
+ .object({
309
+ configOptions: z.array(SessionConfigOptionSchema),
101
310
  _meta: MetaSchema,
102
311
  })
103
312
  .passthrough();
@@ -184,6 +393,13 @@ const UsageUpdate = z
184
393
  _meta: MetaSchema,
185
394
  })
186
395
  .passthrough();
396
+ const ConfigOptionUpdate = z
397
+ .object({
398
+ sessionUpdate: z.literal("config_option_update"),
399
+ configOptions: z.array(SessionConfigOptionSchema),
400
+ _meta: MetaSchema,
401
+ })
402
+ .passthrough();
187
403
  const KNOWN_UPDATE_SCHEMAS = {
188
404
  user_message_chunk: MessageChunkUpdate("user_message_chunk"),
189
405
  agent_message_chunk: MessageChunkUpdate("agent_message_chunk"),
@@ -193,6 +409,7 @@ const KNOWN_UPDATE_SCHEMAS = {
193
409
  plan: PlanUpdate,
194
410
  available_commands_update: AvailableCommandsUpdate,
195
411
  current_mode_update: CurrentModeUpdate,
412
+ config_option_update: ConfigOptionUpdate,
196
413
  usage_update: UsageUpdate,
197
414
  };
198
415
  export const SessionUpdateSchema = z
@@ -227,7 +444,6 @@ export const KNOWN_SESSION_UPDATE_VARIANTS = [
227
444
  "available_commands_update",
228
445
  "current_mode_update",
229
446
  "config_option_update",
230
- "session_info_update",
231
447
  "usage_update",
232
448
  ];
233
449
  export function isUnknownSessionUpdate(update) {
@@ -333,3 +549,24 @@ export function parseWriteTextFileRequest(value, provider) {
333
549
  provider,
334
550
  });
335
551
  }
552
+ export function parseSessionResumeResponse(value, provider) {
553
+ return parseAcp(SessionResumeResponseSchema, value, { method: "session/resume", provider });
554
+ }
555
+ export function parseListSessionsResponse(value, provider) {
556
+ return parseAcp(ListSessionsResponseSchema, value, { method: "session/list", provider });
557
+ }
558
+ export function parseCloseSessionResponse(value, provider) {
559
+ return parseAcp(CloseSessionResponseSchema, value, { method: "session/close", provider });
560
+ }
561
+ export function parseDeleteSessionResponse(value, provider) {
562
+ return parseAcp(DeleteSessionResponseSchema, value, { method: "session/delete", provider });
563
+ }
564
+ export function parseSetSessionModeResponse(value, provider) {
565
+ return parseAcp(SetSessionModeResponseSchema, value, { method: "session/set_mode", provider });
566
+ }
567
+ export function parseSetSessionConfigOptionResponse(value, provider) {
568
+ return parseAcp(SetSessionConfigOptionResponseSchema, value, {
569
+ method: "session/set_config_option",
570
+ provider,
571
+ });
572
+ }
@@ -69,6 +69,8 @@ export interface AsyncJobResult extends AsyncJobSnapshot {
69
69
  responseId?: string | null;
70
70
  model?: string;
71
71
  errorBody?: string;
72
+ providerSessionId?: string;
73
+ stopReason?: string;
72
74
  }
73
75
  export interface StartJobOptions {
74
76
  cwd?: string;
@@ -5,6 +5,7 @@ import { ProcessMonitor } from "./process-monitor.js";
5
5
  import { computeRequestKey, isValidationRunStore } from "./job-store.js";
6
6
  import { NoopFlightRecorder, } from "./flight-recorder.js";
7
7
  import { codexFrResponse } from "./codex-json-parser.js";
8
+ import { extractProviderOutputMetadata } from "./provider-output-metadata.js";
8
9
  import { getRequestContext, resolveOwnerPrincipal, principalCanAccess } from "./request-context.js";
9
10
  import { runApiRequest, ApiHttpError, } from "./api-provider.js";
10
11
  export function extractApiHttpStatus(error) {
@@ -681,6 +682,9 @@ export class AsyncJobManager {
681
682
  const errorMessage = isFailure
682
683
  ? (overrideErrorMessage ?? job.error ?? job.stderr ?? `Exit code ${exitCode}`)
683
684
  : undefined;
685
+ const providerMeta = finalStatus === "completed" && job.transport === "process"
686
+ ? extractProviderOutputMetadata(job.cli, job.stdout, job.outputFormat)
687
+ : undefined;
684
688
  try {
685
689
  this.flightRecorder.logComplete(job.correlationId, {
686
690
  response,
@@ -697,6 +701,8 @@ export class AsyncJobManager {
697
701
  cacheReadTokens: usage.cacheReadTokens,
698
702
  cacheCreationTokens: usage.cacheCreationTokens,
699
703
  costUsd: usage.costUsd,
704
+ providerSessionId: providerMeta?.sessionId,
705
+ stopReason: providerMeta?.stopReason,
700
706
  });
701
707
  job.flightRecorderComplete = true;
702
708
  job.flightRecorderEntry = undefined;
@@ -1136,12 +1142,17 @@ export class AsyncJobManager {
1136
1142
  }
1137
1143
  const stdout = truncateText(job.stdout, maxChars);
1138
1144
  const stderr = truncateText(job.stderr, maxChars);
1145
+ const providerMeta = job.transport === "process" && job.status === "completed"
1146
+ ? extractProviderOutputMetadata(job.cli, job.stdout, job.outputFormat)
1147
+ : undefined;
1139
1148
  return {
1140
1149
  ...this.snapshot(job),
1141
1150
  stdout: stdout.text,
1142
1151
  stderr: stderr.text,
1143
1152
  stdoutTruncated: stdout.truncated,
1144
1153
  stderrTruncated: stderr.truncated,
1154
+ ...(providerMeta?.sessionId ? { providerSessionId: providerMeta.sessionId } : {}),
1155
+ ...(providerMeta?.stopReason ? { stopReason: providerMeta.stopReason } : {}),
1145
1156
  ...(job.transport === "http"
1146
1157
  ? {
1147
1158
  apiUsage: job.apiUsage,
@@ -9,6 +9,7 @@ export interface CodexJsonParseResult {
9
9
  usage?: CodexUsage;
10
10
  error?: string;
11
11
  threadId?: string;
12
+ stopReason?: string;
12
13
  finalMessage?: string;
13
14
  sawEvent?: boolean;
14
15
  }
@@ -47,6 +47,12 @@ export function parseCodexJsonStream(stdout) {
47
47
  }
48
48
  result.usage = usage;
49
49
  }
50
+ if (typeof parsed.stop_reason === "string") {
51
+ result.stopReason = parsed.stop_reason;
52
+ }
53
+ else if (typeof parsed.reason === "string") {
54
+ result.stopReason = parsed.reason;
55
+ }
50
56
  break;
51
57
  }
52
58
  case "turn.failed": {
package/dist/config.d.ts CHANGED
@@ -155,6 +155,7 @@ export interface AcpConfig {
155
155
  promptTimeoutMs: number;
156
156
  allowWriteHostServices: boolean;
157
157
  allowTerminalHostServices: boolean;
158
+ allowMutatingSessionOps: boolean;
158
159
  fallbackToCliWhenUnhealthy: boolean;
159
160
  providers: Record<string, AcpProviderConfig>;
160
161
  sources: {
@@ -162,4 +163,19 @@ export interface AcpConfig {
162
163
  };
163
164
  }
164
165
  export declare function loadAcpConfig(logger?: Logger): AcpConfig;
166
+ export interface AdminConfig {
167
+ allowMutatingCliAdminOps: boolean;
168
+ sources: {
169
+ configFile: string | null;
170
+ };
171
+ }
172
+ export declare function loadAdminConfig(logger?: Logger): AdminConfig;
173
+ export type RemoteOAuthConfigStatus = "absent" | "disabled" | "enabled" | "malformed";
174
+ export interface RemoteOAuthConfigDiagnostics {
175
+ config: RemoteOAuthConfig;
176
+ status: RemoteOAuthConfigStatus;
177
+ configured: boolean;
178
+ issues: string[];
179
+ }
180
+ export declare function diagnoseRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfigDiagnostics;
165
181
  export declare function loadRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfig;
package/dist/config.js CHANGED
@@ -537,6 +537,7 @@ const AcpConfigSchema = z
537
537
  prompt_timeout_ms: z.number().int().positive().default(DEFAULT_ACP_PROMPT_TIMEOUT_MS),
538
538
  allow_write_host_services: z.boolean().default(false),
539
539
  allow_terminal_host_services: z.boolean().default(false),
540
+ allow_mutating_session_ops: z.boolean().default(false),
540
541
  fallback_to_cli_when_unhealthy: z.boolean().default(true),
541
542
  providers: z.record(z.string(), AcpProviderSchema).default({}),
542
543
  })
@@ -552,6 +553,7 @@ function defaultAcpConfig(sourcePath) {
552
553
  promptTimeoutMs: DEFAULT_ACP_PROMPT_TIMEOUT_MS,
553
554
  allowWriteHostServices: false,
554
555
  allowTerminalHostServices: false,
556
+ allowMutatingSessionOps: false,
555
557
  fallbackToCliWhenUnhealthy: true,
556
558
  providers: {},
557
559
  sources: { configFile: sourcePath },
@@ -608,11 +610,56 @@ export function loadAcpConfig(logger = noopLogger) {
608
610
  promptTimeoutMs: parsed.prompt_timeout_ms,
609
611
  allowWriteHostServices: parsed.allow_write_host_services,
610
612
  allowTerminalHostServices: parsed.allow_terminal_host_services,
613
+ allowMutatingSessionOps: parsed.allow_mutating_session_ops,
611
614
  fallbackToCliWhenUnhealthy: parsed.fallback_to_cli_when_unhealthy,
612
615
  providers,
613
616
  sources: { configFile: sourcePath },
614
617
  };
615
618
  }
619
+ const AdminConfigSchema = z
620
+ .object({
621
+ allow_mutating_cli_admin_ops: z.boolean().default(false),
622
+ })
623
+ .strict();
624
+ function defaultAdminConfig(sourcePath) {
625
+ return { allowMutatingCliAdminOps: false, sources: { configFile: sourcePath } };
626
+ }
627
+ function readAdminFile(configPath, logger) {
628
+ if (!existsSync(configPath)) {
629
+ return { raw: undefined, sourcePath: null };
630
+ }
631
+ try {
632
+ const require = createRequire(import.meta.url);
633
+ const TOML = require("smol-toml");
634
+ const text = readFileSync(configPath, "utf-8");
635
+ const parsed = TOML.parse(text);
636
+ return { raw: parsed?.admin, sourcePath: configPath };
637
+ }
638
+ catch (err) {
639
+ logger.error(`Failed to parse gateway config at ${configPath}; using admin defaults`, err);
640
+ return { raw: undefined, sourcePath: null };
641
+ }
642
+ }
643
+ export function loadAdminConfig(logger = noopLogger) {
644
+ const configPath = defaultGatewayConfigPath();
645
+ const { raw, sourcePath } = readAdminFile(configPath, logger);
646
+ if (raw === undefined) {
647
+ return defaultAdminConfig(sourcePath);
648
+ }
649
+ let parsed;
650
+ try {
651
+ parsed = AdminConfigSchema.parse(raw);
652
+ }
653
+ catch (err) {
654
+ throw new Error(`Invalid [admin] config: ${err instanceof Error ? err.message : String(err)}`, {
655
+ cause: err,
656
+ });
657
+ }
658
+ return {
659
+ allowMutatingCliAdminOps: parsed.allow_mutating_cli_admin_ops,
660
+ sources: { configFile: sourcePath },
661
+ };
662
+ }
616
663
  const OAuthRegistrationPolicySchema = z.enum(["static_clients", "shared_secret", "open_dev"]);
617
664
  const OAuthClientSchema = z
618
665
  .object({
@@ -663,11 +710,7 @@ function disabledOAuthConfig(sourcePath = null, envOverrides = []) {
663
710
  function isSafeRedirectUri(uri) {
664
711
  return isHttpsOrLoopbackUrl(uri);
665
712
  }
666
- export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
667
- const configPath = defaultGatewayConfigPath();
668
- const { parsed: configFile, sourcePath } = readGatewayTomlFile(configPath, logger, "OAuth");
669
- const rawHttp = configFile?.http ?? {};
670
- const rawOAuth = rawHttp.oauth ?? {};
713
+ function mergeOAuthEnvOverrides(rawOAuth, env) {
671
714
  const envOverrides = [];
672
715
  const merged = { ...rawOAuth };
673
716
  if (env.LLM_GATEWAY_OAUTH_ENABLED !== undefined) {
@@ -695,43 +738,43 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
695
738
  merged.require_consent = merged.require_consent ?? true;
696
739
  envOverrides.push("LLM_GATEWAY_OAUTH_CONSENT_SECRET");
697
740
  }
698
- const parsed = OAuthConfigSchema.safeParse(merged);
699
- if (!parsed.success) {
700
- logWarn(logger, "Invalid [http.oauth] config; remote OAuth disabled", {
701
- error: parsed.error.message,
702
- });
703
- return disabledOAuthConfig(sourcePath, envOverrides);
704
- }
705
- const data = parsed.data;
741
+ return { merged, envOverrides };
742
+ }
743
+ function validateParsedOAuthConfig(data, env, sourcePath, envOverrides, logger, issues) {
706
744
  if (data.issuer !== "auto" && !isHttpsOrLoopbackUrl(data.issuer)) {
707
745
  logWarn(logger, "Invalid [http.oauth].issuer; remote OAuth disabled");
708
- return disabledOAuthConfig(sourcePath, envOverrides);
746
+ issues.push("OAuth issuer must be an https:// URL (or a loopback URL for local testing).");
747
+ return null;
709
748
  }
710
749
  for (const client of data.clients) {
711
750
  if (!data.allow_public_clients && !client.client_secret_hash) {
712
751
  logWarn(logger, "OAuth client secret hash is required when public clients are disabled", {
713
752
  client_id: client.client_id,
714
753
  });
715
- return disabledOAuthConfig(sourcePath, envOverrides);
754
+ issues.push("An OAuth client is missing a client_secret_hash (required when public clients are disabled). Recreate it with `llm-cli-gateway oauth client add`.");
755
+ return null;
716
756
  }
717
757
  if (client.client_secret_hash && !isSecretHash(client.client_secret_hash)) {
718
758
  logWarn(logger, "Invalid OAuth client secret hash; remote OAuth disabled", {
719
759
  client_id: client.client_id,
720
760
  });
721
- return disabledOAuthConfig(sourcePath, envOverrides);
761
+ issues.push("An OAuth client secret hash is not a valid scrypt hash. Rotate it with `llm-cli-gateway oauth client rotate`.");
762
+ return null;
722
763
  }
723
764
  if (client.allowed_redirect_uris.length === 0 ||
724
765
  client.allowed_redirect_uris.some(uri => !isSafeRedirectUri(uri))) {
725
766
  logWarn(logger, "Invalid OAuth client redirect URI; remote OAuth disabled", {
726
767
  client_id: client.client_id,
727
768
  });
728
- return disabledOAuthConfig(sourcePath, envOverrides);
769
+ issues.push("An OAuth client has a missing or non-https/loopback redirect URI. Re-add the client with a valid --redirect-uri.");
770
+ return null;
729
771
  }
730
772
  }
731
773
  if (data.shared_secret?.enabled) {
732
774
  if (!data.shared_secret.secret_hash || !isSecretHash(data.shared_secret.secret_hash)) {
733
775
  logWarn(logger, "Invalid [http.oauth.shared_secret] secret_hash; remote OAuth disabled");
734
- return disabledOAuthConfig(sourcePath, envOverrides);
776
+ issues.push("The OAuth shared-secret hash is missing or invalid. Reset it with `llm-cli-gateway oauth shared-secret set`.");
777
+ return null;
735
778
  }
736
779
  }
737
780
  if (data.registration_policy === "open_dev" && env.LLM_GATEWAY_OAUTH_OPEN_DEV !== "1") {
@@ -740,7 +783,8 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
740
783
  if (data.require_consent) {
741
784
  if (!data.consent_secret_hash || !isSecretHash(data.consent_secret_hash)) {
742
785
  logWarn(logger, "[http.oauth].require_consent is set but consent_secret_hash is missing/invalid; remote OAuth disabled");
743
- return disabledOAuthConfig(sourcePath, envOverrides);
786
+ issues.push("require_consent is set but the consent secret hash is missing or invalid. Set it with `llm-cli-gateway oauth shared-secret set` or LLM_GATEWAY_OAUTH_CONSENT_SECRET.");
787
+ return null;
744
788
  }
745
789
  }
746
790
  return {
@@ -769,3 +813,45 @@ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
769
813
  sources: { configFile: sourcePath, envOverrides },
770
814
  };
771
815
  }
816
+ export function diagnoseRemoteOAuthConfig(logger = noopLogger, env = process.env) {
817
+ const configPath = defaultGatewayConfigPath();
818
+ const { parsed: configFile, sourcePath } = readGatewayTomlFile(configPath, logger, "OAuth");
819
+ const rawHttp = configFile?.http ?? {};
820
+ const rawOAuth = rawHttp.oauth ?? {};
821
+ const { merged, envOverrides } = mergeOAuthEnvOverrides(rawOAuth, env);
822
+ const configured = Object.keys(rawOAuth).length > 0 || envOverrides.length > 0;
823
+ const parsed = OAuthConfigSchema.safeParse(merged);
824
+ if (!parsed.success) {
825
+ logWarn(logger, "Invalid [http.oauth] config; remote OAuth disabled", {
826
+ error: parsed.error.message,
827
+ });
828
+ return {
829
+ config: disabledOAuthConfig(sourcePath, envOverrides),
830
+ status: "malformed",
831
+ configured: true,
832
+ issues: [
833
+ "The [http.oauth] config is invalid (schema validation failed); remote OAuth is disabled.",
834
+ ],
835
+ };
836
+ }
837
+ const data = parsed.data;
838
+ const issues = [];
839
+ const built = validateParsedOAuthConfig(data, env, sourcePath, envOverrides, logger, issues);
840
+ if (!built) {
841
+ return {
842
+ config: disabledOAuthConfig(sourcePath, envOverrides),
843
+ status: data.enabled ? "malformed" : configured ? "disabled" : "absent",
844
+ configured,
845
+ issues: data.enabled ? issues : [],
846
+ };
847
+ }
848
+ const status = built.enabled
849
+ ? "enabled"
850
+ : configured
851
+ ? "disabled"
852
+ : "absent";
853
+ return { config: built, status, configured, issues: [] };
854
+ }
855
+ export function loadRemoteOAuthConfig(logger = noopLogger, env = process.env) {
856
+ return diagnoseRemoteOAuthConfig(logger, env).config;
857
+ }
@@ -0,0 +1,39 @@
1
+ import { type RemoteHttpOAuthReadiness } from "./doctor.js";
2
+ import type { RemoteOAuthConfig } from "./auth.js";
3
+ export declare const CONNECTOR_SETUP_SECRET_WARNING = "Never paste gateway bearer tokens, OAuth client secrets, OAuth access tokens, consent/shared secrets, tunnel tokens, or provider credentials into a remote chat transcript. Only the fields in this packet are safe to paste into a connector UI.";
4
+ export interface ConnectorSetupOptions {
5
+ clientId?: string;
6
+ includeLegacyNoAuth?: boolean;
7
+ }
8
+ export interface ConnectorSetupPacket {
9
+ ok: boolean;
10
+ schema: "remote-connector-setup.v1";
11
+ ready: boolean;
12
+ stage: RemoteHttpOAuthReadiness["stage"];
13
+ auth_mode: RemoteHttpOAuthReadiness["auth_mode"];
14
+ connector: {
15
+ mcp_url: string | null;
16
+ authorization_url: string | null;
17
+ token_url: string | null;
18
+ client_id: string | null;
19
+ client_secret_required: boolean;
20
+ client_secret_source: string | null;
21
+ };
22
+ workspace: RemoteHttpOAuthReadiness["workspace"];
23
+ next_actions: string[];
24
+ warnings: string[];
25
+ legacy_no_auth?: {
26
+ deprecated: true;
27
+ connector_url: string | null;
28
+ note: string;
29
+ };
30
+ }
31
+ export declare function buildConnectorSetupPacket(input: {
32
+ readiness: RemoteHttpOAuthReadiness;
33
+ oauth: RemoteOAuthConfig;
34
+ options?: ConnectorSetupOptions;
35
+ legacyNoAuthUrl?: string | null;
36
+ }): ConnectorSetupPacket;
37
+ export declare function legacyNoAuthConnectorUrl(env?: NodeJS.ProcessEnv): string | null;
38
+ export declare function gatherConnectorSetupPacket(options?: ConnectorSetupOptions, env?: NodeJS.ProcessEnv): ConnectorSetupPacket;
39
+ export declare function renderConnectorSetupSummary(packet: ConnectorSetupPacket): string;