llm-cli-gateway 2.3.0 → 2.5.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/dist/index.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { randomUUID } from "crypto";
5
- import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, chmodSync, } from "fs";
6
7
  import { dirname, join } from "path";
7
8
  import { fileURLToPath } from "url";
8
9
  import { z } from "zod/v3";
@@ -12,12 +13,13 @@ import { parseCodexJsonStream } from "./codex-json-parser.js";
12
13
  import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js";
13
14
  import { parseVibeMetaJson } from "./mistral-meta-json-parser.js";
14
15
  import { homedir } from "os";
15
- import { createSessionManager } from "./session-manager.js";
16
+ import { CLI_TYPES, PROVIDER_TYPES, createSessionManager, } from "./session-manager.js";
16
17
  import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-manager.js";
17
18
  import { ResourceProvider } from "./resources.js";
18
19
  import { PerformanceMetrics } from "./metrics.js";
19
20
  import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
20
- import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, minStableTokensForModel, } from "./config.js";
21
+ import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, defaultGatewayConfigPath, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
22
+ import { createXaiResponse, XaiApiError, } from "./xai-api-provider.js";
21
23
  import { checkHealth } from "./health.js";
22
24
  import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
23
25
  import { AsyncJobManager, } from "./async-job-manager.js";
@@ -31,9 +33,12 @@ import { resolvePromptInput, PromptPartsSchema, assembleClaudeCacheBlocks, } fro
31
33
  import { computeSessionCacheStats, computeTtlRemaining, readPersistedRequest, PERSISTED_REQUEST_DEFAULT_MAX_CHARS, } from "./cache-stats.js";
32
34
  import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
33
35
  import { startHttpGateway } from "./http-transport.js";
36
+ import { getRequestContext } from "./request-context.js";
34
37
  import { printDoctorJson } from "./doctor.js";
38
+ import { createWorkspace, describeWorkspace, getWorkspace, loadWorkspaceRegistry, registerExistingWorkspace, resolveWorkspaceForProvider, validatePathInsideWorkspace, } from "./workspace-registry.js";
39
+ import { generateSecret, hashSecret } from "./oauth.js";
35
40
  import { registerValidationTools } from "./validation-tools.js";
36
- import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildUpstreamContractReport, } from "./upstream-contracts.js";
41
+ import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildProviderSubcommandsCompactCatalog, buildUpstreamContractReport, getCliSubcommandContract, probeInstalledCliContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
37
42
  import { entrypointFileURL } from "./entrypoint-url.js";
38
43
  const logger = {
39
44
  info: (message, ...args) => {
@@ -141,31 +146,32 @@ function loadSkills() {
141
146
  return skills;
142
147
  }
143
148
  const loadedSkills = loadSkills();
144
- export function buildServerInstructions(asyncJobsEnabled) {
149
+ export function buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled = false) {
145
150
  const asyncToolsNote = asyncJobsEnabled ? " | *_request_async (async)" : "";
151
+ const apiToolsNote = grokApiToolsEnabled ? ", grok_api_request" : "";
146
152
  const jobsLine = asyncJobsEnabled ? "Jobs: llm_job_status, llm_job_result, llm_job_cancel\n" : "";
147
153
  const deferralLine = asyncJobsEnabled
148
154
  ? `- Sync auto-defers at ${SYNC_DEADLINE_MS}ms. Poll deferred jobs via llm_job_status/llm_job_result.`
149
155
  : '- Async jobs are DISABLED (persistence.backend = "none"): *_request_async and llm_job_* tools are not registered, and sync requests run to completion (no auto-deferral).';
150
156
  return `llm-cli-gateway: Multi-LLM orchestration via MCP.
151
157
 
152
- Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
158
+ Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request${apiToolsNote} (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
153
159
  Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation, list_available_models | job_status/job_result (validation jobs)
154
160
  ${jobsLine}Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
155
- Other: list_models, cli_versions, upstream_contracts (use --probe-installed after CLI upgrades to detect drift), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request — sync or async — by correlationId)
161
+ Other: list_models, cli_versions, upstream_contracts, provider_subcommands_* (read-only subcommand contract/drift introspection), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request — sync or async — by correlationId)
156
162
 
157
163
  Key behaviors:
158
164
  ${deferralLine}
159
165
  - Sessions: Claude --continue, Gemini --resume, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\` (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
160
166
  - Approval gates: opt-in via approvalStrategy:"mcp_managed".
161
- - Upstream drift detection: After upgrading any provider CLI (especially grok), use the upstream_contracts tool with probeInstalled: true (or the CLI command "llm-cli-gateway contracts --json --probe-installed"). This is the primary reliable way to detect when an installed binary has gained or lost flags compared to the gateway's declared contract. The probe is safe and read-only.
167
+ - Upstream drift detection: After upgrading any provider CLI (especially grok), use upstream_contracts with probeInstalled:true and provider_subcommand_drift for declared subcommand help surfaces. Probes are safe, read-only --help checks.
162
168
  - Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
163
169
 
164
170
  Skills (full docs via MCP resources):
165
171
  ${loadedSkills.map(s => `- skills://${s.name} — ${s.description}`).join("\n")}`;
166
172
  }
167
- function newGatewayMcpServer(asyncJobsEnabled = true) {
168
- return new McpServer({ name: "llm-cli-gateway", version: packageVersion() }, { instructions: buildServerInstructions(asyncJobsEnabled) });
173
+ function newGatewayMcpServer(asyncJobsEnabled = true, grokApiToolsEnabled = false) {
174
+ return new McpServer({ name: "llm-cli-gateway", version: packageVersion() }, { instructions: buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled) });
169
175
  }
170
176
  let sessionManager;
171
177
  let db = null;
@@ -174,6 +180,7 @@ let resourceProvider;
174
180
  let flightRecorder = null;
175
181
  let persistenceConfig = null;
176
182
  let cacheAwarenessConfig = null;
183
+ let providersConfig = null;
177
184
  let jobStore = null;
178
185
  let jobStoreInitialized = false;
179
186
  let asyncJobManager = null;
@@ -190,6 +197,10 @@ function getCacheAwarenessConfig(runtimeLogger = logger) {
190
197
  cacheAwarenessConfig ??= loadCacheAwarenessConfig(runtimeLogger);
191
198
  return cacheAwarenessConfig;
192
199
  }
200
+ function getProvidersConfig(runtimeLogger = logger) {
201
+ providersConfig ??= loadProvidersConfig(runtimeLogger);
202
+ return providersConfig;
203
+ }
193
204
  function getJobStore(runtimeLogger = logger) {
194
205
  if (jobStoreInitialized)
195
206
  return jobStore;
@@ -217,6 +228,7 @@ function getApprovalManager(runtimeLogger = logger) {
217
228
  return approvalManager;
218
229
  }
219
230
  const MCP_SERVER_ENUM = z.enum(CLAUDE_MCP_SERVER_NAMES);
231
+ const CLI_TYPE_ENUM = z.enum(CLI_TYPES);
220
232
  export const MAX_TURNS_SCHEMA = z.number().int().positive().safe().max(10_000);
221
233
  export const MAX_TOKENS_SCHEMA = z.number().int().positive().safe().max(100_000_000);
222
234
  export const MAX_PRICE_SCHEMA = z.number().positive().finite().min(1e-6).max(10_000);
@@ -244,7 +256,13 @@ export const WORKTREE_SCHEMA = z
244
256
  "path. NOTE: callers should `.gitignore` the `.worktrees/` " +
245
257
  "directory in their repo (the gateway does NOT auto-gitignore — " +
246
258
  "see slice λ spec Q4).");
247
- export const SESSION_PROVIDER_VALUES = ["claude", "codex", "gemini", "grok", "mistral"];
259
+ export const WORKSPACE_ALIAS_SCHEMA = z
260
+ .string()
261
+ .min(1)
262
+ .max(64)
263
+ .regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/)
264
+ .describe("Registered workspace alias. Remote clients use aliases, not absolute paths.");
265
+ export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
248
266
  export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
249
267
  let activeServer = null;
250
268
  let activeHttpGateway = null;
@@ -277,8 +295,13 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
277
295
  logger: runtimeLogger,
278
296
  persistence: deps.persistence ?? getPersistenceConfig(runtimeLogger),
279
297
  cacheAwareness: deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger),
298
+ providers: deps.providers ?? getProvidersConfig(runtimeLogger),
299
+ workspaces: deps.workspaces ?? loadWorkspaceRegistry(runtimeLogger),
280
300
  };
281
301
  }
302
+ export function shouldRegisterGrokApiTools(providers) {
303
+ return isXaiProviderEnabled(providers);
304
+ }
282
305
  const CLI_IDLE_TIMEOUTS = {
283
306
  claude: 600_000,
284
307
  codex: 600_000,
@@ -403,7 +426,7 @@ function buildDeferredToolResponse(deferred, sessionId) {
403
426
  ],
404
427
  };
405
428
  }
406
- export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime) {
429
+ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime, options = {}) {
407
430
  if (!worktreeOpt)
408
431
  return {};
409
432
  const sessionManager = runtime.sessionManager;
@@ -411,12 +434,21 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
411
434
  const session = await Promise.resolve(sessionManager.getSession(sessionId));
412
435
  const existingPath = session?.metadata?.worktreePath;
413
436
  if (typeof existingPath === "string" && existingPath.length > 0) {
414
- return { cwd: existingPath, worktreePath: existingPath };
437
+ return {
438
+ cwd: existingPath,
439
+ worktreePath: existingPath,
440
+ workspaceAlias: typeof session?.metadata?.workspaceAlias === "string"
441
+ ? session.metadata.workspaceAlias
442
+ : options.workspaceAlias,
443
+ workspaceRoot: typeof session?.metadata?.workspaceRoot === "string"
444
+ ? session.metadata.workspaceRoot
445
+ : options.workspaceRoot,
446
+ };
415
447
  }
416
448
  }
417
449
  const name = worktreeOpt === true ? undefined : worktreeOpt.name;
418
450
  const ref = worktreeOpt === true ? undefined : worktreeOpt.ref;
419
- const repoRoot = process.cwd();
451
+ const repoRoot = options.repoRoot ?? process.cwd();
420
452
  const handle = await createWorktree({
421
453
  repoRoot,
422
454
  name,
@@ -427,13 +459,204 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
427
459
  await Promise.resolve(sessionManager.updateSessionMetadata(sessionId, {
428
460
  worktreePath: handle.path,
429
461
  worktreeName: handle.name,
462
+ ...(options.workspaceAlias ? { workspaceAlias: options.workspaceAlias } : {}),
463
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
430
464
  }));
431
465
  }
432
- return { cwd: handle.path, worktreePath: handle.path };
466
+ return {
467
+ cwd: handle.path,
468
+ worktreePath: handle.path,
469
+ workspaceAlias: options.workspaceAlias,
470
+ workspaceRoot: options.workspaceRoot,
471
+ };
472
+ }
473
+ function isGatewayAppDirCwd() {
474
+ return process.cwd() === join(homedir(), ".llm-cli-gateway");
475
+ }
476
+ async function resolveWorkspaceAndWorktreeForRequest(args) {
477
+ const session = args.sessionId
478
+ ? await Promise.resolve(args.runtime.sessionManager.getSession(args.sessionId))
479
+ : null;
480
+ let workspace;
481
+ if (args.workspace ||
482
+ args.runtime.workspaces.defaultAlias ||
483
+ typeof session?.metadata?.workspaceAlias === "string") {
484
+ workspace = resolveWorkspaceForProvider(args.runtime.workspaces, args.provider, args.workspace, session?.metadata);
485
+ }
486
+ else if (isGatewayAppDirCwd()) {
487
+ throw new Error("No workspace selected. Configure [workspaces].default or pass a registered workspace alias.");
488
+ }
489
+ if (!workspace && getRequestContext()?.authKind === "oauth") {
490
+ throw new Error("Remote OAuth provider requests require a registered workspace alias or [workspaces].default.");
491
+ }
492
+ if (!workspace &&
493
+ (args.workingDir || (args.addDir?.length ?? 0) > 0) &&
494
+ !args.runtime.workspaces.allowUnregisteredWorkingDir) {
495
+ throw new Error("workingDir/addDir require a registered workspace alias unless [workspaces].allow_unregistered_working_dir is explicitly enabled.");
496
+ }
497
+ if (workspace) {
498
+ if (args.workingDir) {
499
+ validatePathInsideWorkspace(workspace, args.workingDir, "workingDir");
500
+ }
501
+ for (const dir of args.addDir ?? []) {
502
+ validatePathInsideWorkspace(workspace, dir, "addDir");
503
+ }
504
+ }
505
+ if (args.worktree) {
506
+ if (workspace && !workspace.repo.allowWorktree) {
507
+ throw new Error(`Workspace "${workspace.alias}" does not allow worktree requests`);
508
+ }
509
+ const resolved = await resolveWorktreeForRequest(args.worktree, args.sessionId, args.runtime, {
510
+ repoRoot: workspace?.root,
511
+ workspaceAlias: workspace?.alias,
512
+ workspaceRoot: workspace?.root,
513
+ });
514
+ return { cwd: resolved.cwd, worktreePath: resolved.worktreePath, workspace };
515
+ }
516
+ if (workspace && args.sessionId) {
517
+ await Promise.resolve(args.runtime.sessionManager.updateSessionMetadata(args.sessionId, {
518
+ workspaceAlias: workspace.alias,
519
+ workspaceRoot: workspace.root,
520
+ }));
521
+ }
522
+ return { cwd: workspace?.cwd, workspace };
433
523
  }
434
524
  export function formatWorktreePrefix(worktreePath) {
435
525
  return worktreePath ? `[gateway] worktree=${worktreePath}\n` : "";
436
526
  }
527
+ function workspaceAdminEnabled() {
528
+ const scopes = getRequestContext()?.authScopes ?? [];
529
+ return process.env.LLM_GATEWAY_WORKSPACE_ADMIN === "1" && scopes.includes("workspace:admin");
530
+ }
531
+ function registerWorkspaceTools(server, runtime) {
532
+ server.tool("workspace_list", "List registered workspace aliases and summary metadata. Does not browse files.", {}, {
533
+ title: "List workspaces",
534
+ readOnlyHint: true,
535
+ destructiveHint: false,
536
+ idempotentHint: true,
537
+ openWorldHint: false,
538
+ }, async () => {
539
+ const registry = loadWorkspaceRegistry(runtime.logger);
540
+ return {
541
+ content: [
542
+ {
543
+ type: "text",
544
+ text: JSON.stringify({
545
+ success: true,
546
+ enabled: registry.enabled,
547
+ default: registry.defaultAlias,
548
+ workspaces: registry.repos.map(describeWorkspace),
549
+ allowed_roots: registry.allowedRoots.map(root => ({
550
+ alias: root.alias,
551
+ path: root.path,
552
+ allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
553
+ allow_create_directories: root.allowCreateDirectories,
554
+ allow_init_git_repos: root.allowInitGitRepos,
555
+ max_create_depth: root.maxCreateDepth,
556
+ })),
557
+ }, null, 2),
558
+ },
559
+ ],
560
+ };
561
+ });
562
+ server.tool("workspace_get", "Inspect a registered workspace alias. Does not list files.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
563
+ title: "Get workspace",
564
+ readOnlyHint: true,
565
+ destructiveHint: false,
566
+ idempotentHint: true,
567
+ openWorldHint: false,
568
+ }, async ({ alias }) => {
569
+ try {
570
+ const registry = loadWorkspaceRegistry(runtime.logger);
571
+ return {
572
+ content: [
573
+ {
574
+ type: "text",
575
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(getWorkspace(registry, alias)) }, null, 2),
576
+ },
577
+ ],
578
+ };
579
+ }
580
+ catch (error) {
581
+ return createErrorResponse("workspace_get", 1, "", undefined, error);
582
+ }
583
+ });
584
+ server.tool("workspace_create", "Create a new local folder or git repo under a configured allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
585
+ alias: WORKSPACE_ALIAS_SCHEMA,
586
+ root: WORKSPACE_ALIAS_SCHEMA.describe("Allowed-root alias from workspace_list."),
587
+ slug: z.string().min(1).max(255).describe("Safe relative path under the allowed root."),
588
+ kind: z.enum(["folder", "git"]).default("git"),
589
+ setDefault: z.boolean().default(false),
590
+ }, {
591
+ title: "Create workspace",
592
+ readOnlyHint: false,
593
+ destructiveHint: true,
594
+ idempotentHint: false,
595
+ openWorldHint: false,
596
+ }, async ({ alias, root, slug, kind, setDefault }) => {
597
+ try {
598
+ if (!workspaceAdminEnabled()) {
599
+ throw new Error("workspace_create requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
600
+ }
601
+ const repo = createWorkspace({
602
+ alias,
603
+ rootAlias: root,
604
+ slug,
605
+ kind,
606
+ setDefault,
607
+ logger: runtime.logger,
608
+ });
609
+ return {
610
+ content: [
611
+ {
612
+ type: "text",
613
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(repo) }, null, 2),
614
+ },
615
+ ],
616
+ };
617
+ }
618
+ catch (error) {
619
+ return createErrorResponse("workspace_create", 1, "", undefined, error);
620
+ }
621
+ });
622
+ server.tool("workspace_register_existing_repo", "Register an existing local Git repo under an allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
623
+ alias: WORKSPACE_ALIAS_SCHEMA,
624
+ path: z
625
+ .string()
626
+ .min(1)
627
+ .describe("Absolute path to an existing Git repo under an allowed root."),
628
+ setDefault: z.boolean().default(false),
629
+ }, {
630
+ title: "Register workspace",
631
+ readOnlyHint: false,
632
+ destructiveHint: false,
633
+ idempotentHint: false,
634
+ openWorldHint: false,
635
+ }, async ({ alias, path, setDefault }) => {
636
+ try {
637
+ if (!workspaceAdminEnabled()) {
638
+ throw new Error("workspace_register_existing_repo requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
639
+ }
640
+ const repo = registerExistingWorkspace({
641
+ alias,
642
+ repoPath: path,
643
+ setDefault,
644
+ logger: runtime.logger,
645
+ });
646
+ return {
647
+ content: [
648
+ {
649
+ type: "text",
650
+ text: JSON.stringify({ success: true, workspace: describeWorkspace(repo) }, null, 2),
651
+ },
652
+ ],
653
+ };
654
+ }
655
+ catch (error) {
656
+ return createErrorResponse("workspace_register_existing_repo", 1, "", undefined, error);
657
+ }
658
+ });
659
+ }
437
660
  function createErrorResponse(cli, code, stderr, correlationId, error) {
438
661
  let errorMessage = `Error executing ${cli} CLI`;
439
662
  const isLaunchExit = code === 127 || code === -4058;
@@ -741,12 +964,12 @@ function registerBaseResources(server, runtime) {
741
964
  const contents = await runtime.resourceProvider.readResource(uri.href);
742
965
  return { contents: contents ? [contents] : [] };
743
966
  });
744
- server.registerResource("cache-state-global", "cache_state://global", {
967
+ server.registerResource("cache-state-global", "cache-state://global", {
745
968
  title: "💾 Cache State (Global)",
746
969
  description: "Aggregate cache hit/miss/savings across all CLIs in the flight recorder. Tokens/hashes only — no prompt text.",
747
970
  mimeType: "application/json",
748
971
  }, async (uri) => {
749
- runtime.logger.debug("Reading cache_state://global resource");
972
+ runtime.logger.debug("Reading cache-state://global resource");
750
973
  const stats = runtime.resourceProvider.readCacheStateGlobal({
751
974
  lastNHours: 24,
752
975
  });
@@ -760,7 +983,7 @@ function registerBaseResources(server, runtime) {
760
983
  ],
761
984
  };
762
985
  });
763
- server.registerResource("cache-state-session", new ResourceTemplate("cache_state://session/{sessionId}", { list: undefined }), {
986
+ server.registerResource("cache-state-session", new ResourceTemplate("cache-state://session/{sessionId}", { list: undefined }), {
764
987
  title: "💾 Cache State (Session)",
765
988
  description: "Per-session cache hit/miss/savings. Tokens/hashes only — no prompt text.",
766
989
  mimeType: "application/json",
@@ -768,7 +991,7 @@ function registerBaseResources(server, runtime) {
768
991
  const sessionId = Array.isArray(variables.sessionId)
769
992
  ? variables.sessionId[0]
770
993
  : variables.sessionId;
771
- runtime.logger.debug(`Reading cache_state://session/${sessionId}`);
994
+ runtime.logger.debug(`Reading cache-state://session/${sessionId}`);
772
995
  const stats = runtime.resourceProvider.readCacheStateSession(String(sessionId));
773
996
  return {
774
997
  contents: [
@@ -780,13 +1003,13 @@ function registerBaseResources(server, runtime) {
780
1003
  ],
781
1004
  };
782
1005
  });
783
- server.registerResource("cache-state-prefix", new ResourceTemplate("cache_state://prefix/{hash}", { list: undefined }), {
1006
+ server.registerResource("cache-state-prefix", new ResourceTemplate("cache-state://prefix/{hash}", { list: undefined }), {
784
1007
  title: "💾 Cache State (Prefix)",
785
1008
  description: "Per-stable-prefix-hash cache hit/miss/savings, with CLI breakdown. Tokens/hashes only — no prompt text.",
786
1009
  mimeType: "application/json",
787
1010
  }, async (uri, variables) => {
788
1011
  const hash = Array.isArray(variables.hash) ? variables.hash[0] : variables.hash;
789
- runtime.logger.debug(`Reading cache_state://prefix/${hash}`);
1012
+ runtime.logger.debug(`Reading cache-state://prefix/${hash}`);
790
1013
  const stats = runtime.resourceProvider.readCacheStateForPrefix(String(hash));
791
1014
  return {
792
1015
  contents: [
@@ -798,6 +1021,30 @@ function registerBaseResources(server, runtime) {
798
1021
  ],
799
1022
  };
800
1023
  });
1024
+ server.registerResource("provider-subcommands-catalog", "provider-subcommands://catalog", {
1025
+ title: "Provider Subcommands Catalog",
1026
+ description: "Compact read-only catalog of declared provider CLI subcommands",
1027
+ mimeType: "application/json",
1028
+ }, async (uri) => {
1029
+ runtime.logger.debug("Reading provider-subcommands://catalog resource");
1030
+ const contents = await runtime.resourceProvider.readResource(uri.href);
1031
+ return { contents: contents ? [contents] : [] };
1032
+ });
1033
+ server.registerResource("provider-subcommand-contract", new ResourceTemplate("provider-subcommands://{provider}/{+commandPath}", { list: undefined }), {
1034
+ title: "Provider Subcommand Contract",
1035
+ description: "Detailed read-only contract for one declared provider CLI subcommand",
1036
+ mimeType: "application/json",
1037
+ }, async (uri, variables) => {
1038
+ const provider = Array.isArray(variables.provider)
1039
+ ? variables.provider[0]
1040
+ : variables.provider;
1041
+ const commandPath = Array.isArray(variables.commandPath)
1042
+ ? variables.commandPath[0]
1043
+ : variables.commandPath;
1044
+ runtime.logger.debug(`Reading provider-subcommands://${provider}/${commandPath}`);
1045
+ const contents = await runtime.resourceProvider.readResource(uri.href);
1046
+ return { contents: contents ? [contents] : [] };
1047
+ });
801
1048
  }
802
1049
  function resolvePromptOrPartsForPrep(args) {
803
1050
  const hasPrompt = typeof args.prompt === "string" && args.prompt.length > 0;
@@ -1676,6 +1923,271 @@ function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep
1676
1923
  }
1677
1924
  return response;
1678
1925
  }
1926
+ function buildXaiPromptPartsUserContent(promptParts) {
1927
+ const userSections = [];
1928
+ if (promptParts.tools && promptParts.tools.length > 0) {
1929
+ userSections.push(`<tools>\n${promptParts.tools}\n</tools>`);
1930
+ }
1931
+ if (promptParts.context && promptParts.context.length > 0) {
1932
+ userSections.push(`<context>\n${promptParts.context}\n</context>`);
1933
+ }
1934
+ if (promptParts.task && promptParts.task.length > 0) {
1935
+ userSections.push(promptParts.task);
1936
+ }
1937
+ return userSections.join("\n\n");
1938
+ }
1939
+ function buildXaiPromptPartsEffectivePrompt(instructions, userContent) {
1940
+ return instructions && instructions.length > 0
1941
+ ? `${instructions}\n\n${userContent}`
1942
+ : userContent;
1943
+ }
1944
+ function prepareGrokApiRequest(params, providers) {
1945
+ const corrId = params.correlationId || randomUUID();
1946
+ if (!providers.xai) {
1947
+ return createErrorResponse("grok_api_request", 1, "", corrId, new Error("[providers.xai] is not configured"));
1948
+ }
1949
+ const inputResolution = resolvePromptOrPartsForPrep({
1950
+ prompt: params.prompt,
1951
+ promptParts: params.promptParts,
1952
+ operation: "grok_api_request",
1953
+ correlationId: corrId,
1954
+ });
1955
+ if (!inputResolution.ok)
1956
+ return inputResolution.error;
1957
+ const instructions = params.promptParts?.system && params.promptParts.system.length > 0
1958
+ ? params.promptParts.system
1959
+ : undefined;
1960
+ let effectivePrompt = inputResolution.assembledPrompt;
1961
+ let input;
1962
+ if (params.promptParts) {
1963
+ let userContent = buildXaiPromptPartsUserContent(params.promptParts);
1964
+ if (params.optimizePrompt) {
1965
+ const optimized = optimizePromptText(userContent);
1966
+ logOptimizationTokens("prompt", corrId, userContent, optimized);
1967
+ userContent = optimized;
1968
+ }
1969
+ effectivePrompt = buildXaiPromptPartsEffectivePrompt(instructions, userContent);
1970
+ input = [{ role: "user", content: userContent }];
1971
+ }
1972
+ else {
1973
+ if (params.optimizePrompt) {
1974
+ const optimized = optimizePromptText(effectivePrompt);
1975
+ logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
1976
+ effectivePrompt = optimized;
1977
+ }
1978
+ input = effectivePrompt;
1979
+ }
1980
+ const resolvedModel = params.model ?? providers.xai.defaultModel;
1981
+ if (params.reasoningEffort && !/^grok-4\.3(?:$|[-.])/.test(resolvedModel)) {
1982
+ return createErrorResponse("grok_api_request", 1, "", corrId, new Error("reasoningEffort is currently supported only for xAI model grok-4.3"));
1983
+ }
1984
+ return {
1985
+ corrId,
1986
+ effectivePrompt,
1987
+ resolvedModel,
1988
+ instructions,
1989
+ input,
1990
+ stablePrefixHash: inputResolution.stablePrefixHash,
1991
+ stablePrefixTokens: inputResolution.stablePrefixTokens,
1992
+ };
1993
+ }
1994
+ function usageFromXaiResult(result) {
1995
+ return {
1996
+ inputTokens: result.usage.inputTokens,
1997
+ outputTokens: result.usage.outputTokens,
1998
+ cacheReadTokens: result.usage.cacheReadTokens,
1999
+ costUsd: result.usage.costUsd,
2000
+ };
2001
+ }
2002
+ async function getExistingSessionForProvider(sessionManager, sessionId, provider) {
2003
+ if (!sessionId)
2004
+ return null;
2005
+ const existing = await sessionManager.getSession(sessionId);
2006
+ if (existing && existing.cli !== provider) {
2007
+ throw new Error(`Session ${sessionId} belongs to provider '${existing.cli}', not '${provider}'`);
2008
+ }
2009
+ return existing;
2010
+ }
2011
+ function asXaiApiError(error) {
2012
+ if (error instanceof XaiApiError)
2013
+ return error;
2014
+ const cause = error?.cause;
2015
+ return cause instanceof XaiApiError ? cause : null;
2016
+ }
2017
+ function buildGrokApiToolResponse(args) {
2018
+ let text = args.result.text;
2019
+ if (args.optimizeResponse) {
2020
+ const optimized = optimizeResponseText(text);
2021
+ logOptimizationTokens("response", args.corrId, text, optimized);
2022
+ text = optimized;
2023
+ }
2024
+ const response = {
2025
+ content: [{ type: "text", text }],
2026
+ structuredContent: {
2027
+ provider: "grok-api",
2028
+ cli: "grok-api",
2029
+ model: args.result.model || args.prep.resolvedModel,
2030
+ correlationId: args.corrId,
2031
+ sessionId: args.sessionId || null,
2032
+ responseId: args.result.responseId,
2033
+ previousResponseId: args.previousResponseId || null,
2034
+ stalePreviousResponseCleared: args.stalePreviousResponseCleared,
2035
+ status: args.result.status,
2036
+ httpStatus: args.result.httpStatus,
2037
+ durationMs: args.durationMs,
2038
+ ...usageFromXaiResult(args.result),
2039
+ exitCode: 0,
2040
+ retryCount: 0,
2041
+ },
2042
+ };
2043
+ if (args.sessionId)
2044
+ response.sessionId = args.sessionId;
2045
+ return response;
2046
+ }
2047
+ async function resolveGrokApiSession(params, runtime) {
2048
+ if (params.sessionId) {
2049
+ const existing = await getExistingSessionForProvider(runtime.sessionManager, params.sessionId, "grok-api");
2050
+ const session = existing ??
2051
+ (await runtime.sessionManager.createSession("grok-api", "Grok API Session", params.sessionId));
2052
+ const previous = !params.createNewSession && typeof session.metadata?.xaiPreviousResponseId === "string"
2053
+ ? session.metadata.xaiPreviousResponseId
2054
+ : undefined;
2055
+ return { sessionId: session.id, previousResponseId: previous };
2056
+ }
2057
+ if (!params.createNewSession) {
2058
+ const active = await runtime.sessionManager.getActiveSession("grok-api");
2059
+ if (active) {
2060
+ const previous = typeof active.metadata?.xaiPreviousResponseId === "string"
2061
+ ? active.metadata.xaiPreviousResponseId
2062
+ : undefined;
2063
+ return { sessionId: active.id, previousResponseId: previous };
2064
+ }
2065
+ }
2066
+ const session = await runtime.sessionManager.createSession("grok-api", "Grok API Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
2067
+ return { sessionId: session.id };
2068
+ }
2069
+ export async function handleGrokApiRequest(deps, params) {
2070
+ const runtime = resolveHandlerRuntime(deps);
2071
+ const startTime = Date.now();
2072
+ const prep = prepareGrokApiRequest(params, runtime.providers);
2073
+ if ("content" in prep)
2074
+ return prep;
2075
+ const { corrId } = prep;
2076
+ const xaiConfig = runtime.providers.xai;
2077
+ let durationMs = 0;
2078
+ let wasSuccessful = false;
2079
+ try {
2080
+ await getExistingSessionForProvider(runtime.sessionManager, params.sessionId, "grok-api");
2081
+ }
2082
+ catch (err) {
2083
+ return createErrorResponse("grok_api_request", 1, "", corrId, err);
2084
+ }
2085
+ if (!xaiConfig) {
2086
+ return createErrorResponse("grok_api_request", 1, "", corrId, new Error("[providers.xai] is not configured"));
2087
+ }
2088
+ const apiKey = process.env[xaiConfig.apiKeyEnv]?.trim();
2089
+ if (!apiKey) {
2090
+ return createErrorResponse("grok_api_request", 1, "", corrId, new Error(`xAI API key env var ${xaiConfig.apiKeyEnv} is not set`));
2091
+ }
2092
+ safeFlightStart({
2093
+ correlationId: corrId,
2094
+ cli: "grok-api",
2095
+ model: prep.resolvedModel,
2096
+ prompt: prep.effectivePrompt,
2097
+ sessionId: params.sessionId,
2098
+ stablePrefixHash: prep.stablePrefixHash ?? undefined,
2099
+ stablePrefixTokens: prep.stablePrefixTokens ?? undefined,
2100
+ }, runtime);
2101
+ let sessionId;
2102
+ let previousResponseId;
2103
+ let stalePreviousResponseCleared = false;
2104
+ try {
2105
+ const session = await resolveGrokApiSession(params, runtime);
2106
+ sessionId = session.sessionId;
2107
+ previousResponseId = session.previousResponseId;
2108
+ const call = (prev) => createXaiResponse({
2109
+ baseUrl: xaiConfig.baseUrl,
2110
+ apiKey,
2111
+ model: prep.resolvedModel,
2112
+ input: prep.input,
2113
+ instructions: prep.instructions,
2114
+ previousResponseId: prev,
2115
+ maxOutputTokens: params.maxOutputTokens,
2116
+ temperature: params.temperature,
2117
+ topP: params.topP,
2118
+ reasoningEffort: params.reasoningEffort,
2119
+ timeoutMs: params.timeoutMs,
2120
+ }, runtime.logger);
2121
+ let result;
2122
+ try {
2123
+ result = await call(previousResponseId);
2124
+ }
2125
+ catch (error) {
2126
+ const xaiError = asXaiApiError(error);
2127
+ if (xaiError?.status === 404 && previousResponseId) {
2128
+ runtime.logger.warn(`[${corrId}] xAI previous_response_id was rejected; clearing stale session metadata and retrying fresh`);
2129
+ await runtime.sessionManager.updateSessionMetadata(sessionId, {
2130
+ xaiPreviousResponseId: null,
2131
+ xaiResponseCreatedAt: null,
2132
+ });
2133
+ stalePreviousResponseCleared = true;
2134
+ previousResponseId = undefined;
2135
+ result = await call(undefined);
2136
+ }
2137
+ else {
2138
+ throw error;
2139
+ }
2140
+ }
2141
+ durationMs = Math.max(0, Date.now() - startTime);
2142
+ wasSuccessful = true;
2143
+ await runtime.sessionManager.updateSessionMetadata(sessionId, {
2144
+ xaiPreviousResponseId: result.responseId,
2145
+ xaiResponseCreatedAt: new Date().toISOString(),
2146
+ xaiModel: result.model || prep.resolvedModel,
2147
+ });
2148
+ await runtime.sessionManager.updateSessionUsage(sessionId);
2149
+ safeFlightComplete(corrId, {
2150
+ response: result.text,
2151
+ durationMs,
2152
+ retryCount: 0,
2153
+ circuitBreakerState: "closed",
2154
+ optimizationApplied: params.optimizePrompt || (params.optimizeResponse ?? false),
2155
+ exitCode: 0,
2156
+ status: "completed",
2157
+ ...usageFromXaiResult(result),
2158
+ }, runtime);
2159
+ return buildGrokApiToolResponse({
2160
+ result,
2161
+ prep,
2162
+ corrId,
2163
+ durationMs,
2164
+ sessionId,
2165
+ previousResponseId,
2166
+ stalePreviousResponseCleared,
2167
+ optimizeResponse: params.optimizeResponse ?? false,
2168
+ });
2169
+ }
2170
+ catch (error) {
2171
+ durationMs = Math.max(0, Date.now() - startTime);
2172
+ const err = error;
2173
+ const xaiError = asXaiApiError(error);
2174
+ runtime.logger.error(`[${corrId}] grok_api_request failed`, err.message);
2175
+ safeFlightComplete(corrId, {
2176
+ response: xaiError?.responseText ?? "",
2177
+ durationMs,
2178
+ retryCount: 0,
2179
+ circuitBreakerState: "closed",
2180
+ optimizationApplied: false,
2181
+ exitCode: 1,
2182
+ errorMessage: err.message,
2183
+ status: "failed",
2184
+ }, runtime);
2185
+ return createErrorResponse("grok_api_request", 1, "", corrId, err);
2186
+ }
2187
+ finally {
2188
+ runtime.performanceMetrics.recordRequest("grok-api", durationMs || Math.max(0, Date.now() - startTime), wasSuccessful);
2189
+ }
2190
+ }
1679
2191
  function maybeBuildCacheTtlWarning(args) {
1680
2192
  if (args.cli !== "claude")
1681
2193
  return null;
@@ -1715,6 +2227,7 @@ function resolveHandlerRuntime(deps) {
1715
2227
  sessionManager: deps.sessionManager,
1716
2228
  logger: normalizedLogger,
1717
2229
  asyncJobManager: asyncDeps.asyncJobManager,
2230
+ workspaces: deps.workspaces,
1718
2231
  });
1719
2232
  }
1720
2233
  export async function handleGeminiRequest(deps, params) {
@@ -1762,18 +2275,28 @@ export async function handleGeminiRequest(deps, params) {
1762
2275
  resumeLatest: params.resumeLatest,
1763
2276
  createNewSession: params.createNewSession,
1764
2277
  });
1765
- args.push(...sessionPlan.args);
1766
2278
  const userProvidedSession = sessionPlan.resumed;
1767
2279
  const effectiveSessionIdHint = sessionPlan.resumed ? params.sessionId : undefined;
2280
+ if (effectiveSessionIdHint) {
2281
+ await getExistingSessionForProvider(deps.sessionManager, effectiveSessionIdHint, "gemini");
2282
+ }
2283
+ args.push(...sessionPlan.args);
1768
2284
  let worktreeResolution = {};
1769
2285
  try {
1770
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionIdHint, runtime);
2286
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2287
+ provider: "gemini",
2288
+ workspace: params.workspace,
2289
+ worktree: params.worktree,
2290
+ sessionId: effectiveSessionIdHint,
2291
+ runtime,
2292
+ addDir: params.includeDirs,
2293
+ });
1771
2294
  }
1772
2295
  catch (err) {
1773
2296
  return createErrorResponse("gemini_request", 1, "", corrId, err);
1774
2297
  }
1775
2298
  const geminiFrHandoff = buildAsyncFlightRecorderHandoff("gemini", prep, params.sessionId, params.outputFormat);
1776
- const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage, worktreeResolution.cwd);
2299
+ const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
1777
2300
  if (isDeferredResponse(result)) {
1778
2301
  return buildDeferredToolResponse(result, effectiveSessionIdHint);
1779
2302
  }
@@ -1887,11 +2410,11 @@ export async function handleGeminiRequestAsync(deps, params) {
1887
2410
  resumeLatest: params.resumeLatest,
1888
2411
  createNewSession: params.createNewSession,
1889
2412
  });
1890
- args.push(...sessionPlan.args);
1891
2413
  let effectiveSessionId = sessionPlan.resumed ? params.sessionId : undefined;
2414
+ const existingSession = await getExistingSessionForProvider(deps.sessionManager, effectiveSessionId, "gemini");
2415
+ args.push(...sessionPlan.args);
1892
2416
  if (effectiveSessionId) {
1893
- const existing = await deps.sessionManager.getSession(effectiveSessionId);
1894
- if (!existing) {
2417
+ if (!existingSession) {
1895
2418
  try {
1896
2419
  await deps.sessionManager.createSession("gemini", "Gemini Session", effectiveSessionId);
1897
2420
  }
@@ -1905,7 +2428,14 @@ export async function handleGeminiRequestAsync(deps, params) {
1905
2428
  }
1906
2429
  let worktreeResolution = {};
1907
2430
  try {
1908
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2431
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2432
+ provider: "gemini",
2433
+ workspace: params.workspace,
2434
+ worktree: params.worktree,
2435
+ sessionId: effectiveSessionId,
2436
+ runtime,
2437
+ addDir: params.includeDirs,
2438
+ });
1909
2439
  }
1910
2440
  catch (err) {
1911
2441
  return createErrorResponse("gemini_request_async", 1, "", corrId, err);
@@ -2012,10 +2542,20 @@ export async function handleGrokRequest(deps, params) {
2012
2542
  resumeLatest: params.resumeLatest,
2013
2543
  createNewSession: params.createNewSession,
2014
2544
  });
2545
+ if (sessionResult.userProvidedSession) {
2546
+ await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "grok");
2547
+ }
2015
2548
  args.push(...sessionResult.resumeArgs);
2016
2549
  let worktreeResolution = {};
2017
2550
  try {
2018
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
2551
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2552
+ provider: "grok",
2553
+ workspace: params.workspace,
2554
+ worktree: params.worktree,
2555
+ sessionId: sessionResult.effectiveSessionId,
2556
+ runtime,
2557
+ workingDir: params.workingDir,
2558
+ });
2019
2559
  }
2020
2560
  catch (err) {
2021
2561
  return createErrorResponse("grok_request", 1, "", corrId, err);
@@ -2158,6 +2698,9 @@ export async function handleGrokRequestAsync(deps, params) {
2158
2698
  resumeLatest: params.resumeLatest,
2159
2699
  createNewSession: params.createNewSession,
2160
2700
  });
2701
+ if (sessionResult.userProvidedSession) {
2702
+ await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "grok");
2703
+ }
2161
2704
  args.push(...sessionResult.resumeArgs);
2162
2705
  let effectiveSessionId = sessionResult.effectiveSessionId;
2163
2706
  if (sessionResult.userProvidedSession && effectiveSessionId) {
@@ -2180,7 +2723,14 @@ export async function handleGrokRequestAsync(deps, params) {
2180
2723
  }
2181
2724
  let worktreeResolution = {};
2182
2725
  try {
2183
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2726
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2727
+ provider: "grok",
2728
+ workspace: params.workspace,
2729
+ worktree: params.worktree,
2730
+ sessionId: effectiveSessionId,
2731
+ runtime,
2732
+ workingDir: params.workingDir,
2733
+ });
2184
2734
  }
2185
2735
  catch (err) {
2186
2736
  return createErrorResponse("grok_request_async", 1, "", corrId, err);
@@ -2262,10 +2812,21 @@ export async function handleMistralRequest(deps, params) {
2262
2812
  resumeLatest: params.resumeLatest,
2263
2813
  createNewSession: params.createNewSession,
2264
2814
  });
2815
+ if (sessionResult.userProvidedSession) {
2816
+ await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "mistral");
2817
+ }
2265
2818
  args.push(...sessionResult.resumeArgs);
2266
2819
  let worktreeResolution = {};
2267
2820
  try {
2268
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
2821
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2822
+ provider: "mistral",
2823
+ workspace: params.workspace,
2824
+ worktree: params.worktree,
2825
+ sessionId: sessionResult.effectiveSessionId,
2826
+ runtime,
2827
+ workingDir: params.workingDir,
2828
+ addDir: params.addDir,
2829
+ });
2269
2830
  }
2270
2831
  catch (err) {
2271
2832
  return createErrorResponse("mistral_request", 1, "", corrId, err);
@@ -2397,11 +2958,11 @@ export async function handleMistralRequestAsync(deps, params) {
2397
2958
  resumeLatest: params.resumeLatest,
2398
2959
  createNewSession: params.createNewSession,
2399
2960
  });
2400
- args.push(...sessionResult.resumeArgs);
2401
2961
  let effectiveSessionId = sessionResult.effectiveSessionId;
2962
+ const existingSession = await getExistingSessionForProvider(deps.sessionManager, sessionResult.userProvidedSession ? effectiveSessionId : undefined, "mistral");
2963
+ args.push(...sessionResult.resumeArgs);
2402
2964
  if (sessionResult.userProvidedSession && effectiveSessionId) {
2403
- const existing = await deps.sessionManager.getSession(effectiveSessionId);
2404
- if (!existing) {
2965
+ if (!existingSession) {
2405
2966
  try {
2406
2967
  await deps.sessionManager.createSession("mistral", "Mistral Session", effectiveSessionId);
2407
2968
  }
@@ -2419,7 +2980,15 @@ export async function handleMistralRequestAsync(deps, params) {
2419
2980
  }
2420
2981
  let worktreeResolution = {};
2421
2982
  try {
2422
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
2983
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
2984
+ provider: "mistral",
2985
+ workspace: params.workspace,
2986
+ worktree: params.worktree,
2987
+ sessionId: effectiveSessionId,
2988
+ runtime,
2989
+ workingDir: params.workingDir,
2990
+ addDir: params.addDir,
2991
+ });
2423
2992
  }
2424
2993
  catch (err) {
2425
2994
  return createErrorResponse("mistral_request_async", 1, "", corrId, err);
@@ -2458,6 +3027,12 @@ export async function handleMistralRequestAsync(deps, params) {
2458
3027
  }
2459
3028
  export async function handleCodexRequestAsync(deps, params) {
2460
3029
  const runtime = resolveHandlerRuntime(deps);
3030
+ try {
3031
+ await getExistingSessionForProvider(deps.sessionManager, params.sessionId, "codex");
3032
+ }
3033
+ catch (err) {
3034
+ return createErrorResponse("codex_request_async", 1, "", params.correlationId, err);
3035
+ }
2461
3036
  const prep = prepareCodexRequest({
2462
3037
  prompt: params.prompt,
2463
3038
  promptParts: params.promptParts,
@@ -2525,7 +3100,15 @@ export async function handleCodexRequestAsync(deps, params) {
2525
3100
  }
2526
3101
  let worktreeResolution = {};
2527
3102
  try {
2528
- worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
3103
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3104
+ provider: "codex",
3105
+ workspace: params.workspace,
3106
+ worktree: params.worktree,
3107
+ sessionId: effectiveSessionId,
3108
+ runtime,
3109
+ workingDir: params.workingDir,
3110
+ addDir: params.addDir,
3111
+ });
2529
3112
  }
2530
3113
  catch (err) {
2531
3114
  runPrepCleanupLocally();
@@ -2572,13 +3155,90 @@ export async function handleCodexRequestAsync(deps, params) {
2572
3155
  }
2573
3156
  export function createGatewayServer(deps = {}) {
2574
3157
  const runtime = resolveGatewayServerRuntime(deps, { isolateState: true });
2575
- const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, } = runtime;
3158
+ const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, providers, } = runtime;
2576
3159
  void flightRecorder;
2577
3160
  void cacheAwareness;
3161
+ const grokApiToolsEnabled = shouldRegisterGrokApiTools(providers);
2578
3162
  const asyncJobsEnabled = persistence.backend !== "none" && persistence.asyncJobsEnabled && asyncJobManager.hasStore();
2579
- const server = newGatewayMcpServer(asyncJobsEnabled);
3163
+ const server = newGatewayMcpServer(asyncJobsEnabled, grokApiToolsEnabled);
2580
3164
  registerBaseResources(server, runtime);
2581
3165
  registerValidationTools(server, { asyncJobManager });
3166
+ registerWorkspaceTools(server, runtime);
3167
+ if (grokApiToolsEnabled) {
3168
+ server.tool("grok_api_request", "Run an xAI Grok API request synchronously through the Responses API. Requires exactly one of prompt or promptParts. Registered only when [providers.xai] is configured and its API-key env var is present.", {
3169
+ prompt: z
3170
+ .string()
3171
+ .min(1, "Prompt cannot be empty")
3172
+ .max(100000, "Prompt too long (max 100k chars)")
3173
+ .optional()
3174
+ .describe("Prompt text for xAI Grok API (mutually exclusive with promptParts)"),
3175
+ promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt: { system?, tools?, context?, task }. Mutually exclusive with prompt. The stable prefix hash is logged for cache_state aggregates; xAI does not receive cache_control hints."),
3176
+ model: z
3177
+ .string()
3178
+ .min(1)
3179
+ .optional()
3180
+ .describe("xAI model id; defaults to [providers.xai].default_model"),
3181
+ sessionId: z
3182
+ .string()
3183
+ .optional()
3184
+ .describe("Gateway grok-api session to continue. The gateway stores xAI previous_response_id in session metadata."),
3185
+ createNewSession: z
3186
+ .boolean()
3187
+ .default(false)
3188
+ .describe("Start a fresh xAI response chain. With sessionId, ignores any stored previous_response_id for this request."),
3189
+ correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
3190
+ optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
3191
+ optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
3192
+ maxOutputTokens: MAX_TOKENS_SCHEMA.optional().describe("xAI Responses API max_output_tokens. Bounded to safe integers <= 100000000."),
3193
+ temperature: z
3194
+ .number()
3195
+ .finite()
3196
+ .min(0)
3197
+ .max(2)
3198
+ .optional()
3199
+ .describe("Sampling temperature passed to xAI Responses API"),
3200
+ topP: z
3201
+ .number()
3202
+ .finite()
3203
+ .min(0)
3204
+ .max(1)
3205
+ .optional()
3206
+ .describe("Nucleus sampling top_p passed to xAI Responses API"),
3207
+ reasoningEffort: z
3208
+ .enum(["none", "low", "medium", "high"])
3209
+ .optional()
3210
+ .describe("xAI Responses API reasoning.effort"),
3211
+ timeoutMs: z
3212
+ .number()
3213
+ .int()
3214
+ .min(30_000)
3215
+ .max(3_600_000)
3216
+ .optional()
3217
+ .describe("HTTP request timeout in ms (min 30s, max 1h, default 10m)"),
3218
+ }, {
3219
+ title: "Grok API request",
3220
+ readOnlyHint: false,
3221
+ destructiveHint: false,
3222
+ idempotentHint: false,
3223
+ openWorldHint: true,
3224
+ }, async ({ prompt, promptParts, model, sessionId, createNewSession, correlationId, optimizePrompt, optimizeResponse, maxOutputTokens, temperature, topP, reasoningEffort, timeoutMs, }) => {
3225
+ return handleGrokApiRequest({ sessionManager, logger, runtime }, {
3226
+ prompt,
3227
+ promptParts,
3228
+ model,
3229
+ sessionId,
3230
+ createNewSession,
3231
+ correlationId,
3232
+ optimizePrompt,
3233
+ optimizeResponse,
3234
+ maxOutputTokens,
3235
+ temperature,
3236
+ topP,
3237
+ reasoningEffort,
3238
+ timeoutMs,
3239
+ });
3240
+ });
3241
+ }
2582
3242
  server.tool("claude_request", "Run a Claude Code CLI request synchronously (when async jobs are enabled, auto-defers to a pollable job past the sync deadline; otherwise runs to completion). Requires exactly one of prompt or promptParts.", {
2583
3243
  prompt: z
2584
3244
  .string()
@@ -2687,6 +3347,7 @@ export function createGatewayServer(deps = {}) {
2687
3347
  .array(z.string())
2688
3348
  .optional()
2689
3349
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
3350
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
2690
3351
  worktree: WORKTREE_SCHEMA.optional(),
2691
3352
  approvalStrategy: z
2692
3353
  .enum(["legacy", "mcp_managed"])
@@ -2724,7 +3385,7 @@ export function createGatewayServer(deps = {}) {
2724
3385
  destructiveHint: true,
2725
3386
  idempotentHint: false,
2726
3387
  openWorldHint: true,
2727
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
3388
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, workspace, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
2728
3389
  const startTime = Date.now();
2729
3390
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
2730
3391
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
@@ -2783,6 +3444,12 @@ export function createGatewayServer(deps = {}) {
2783
3444
  if (!useContinue && effectiveSessionId && activeSession?.id === effectiveSessionId) {
2784
3445
  useContinue = true;
2785
3446
  }
3447
+ try {
3448
+ await getExistingSessionForProvider(sessionManager, effectiveSessionId, "claude");
3449
+ }
3450
+ catch (err) {
3451
+ return createErrorResponse("claude_request", 1, "", corrId, err);
3452
+ }
2786
3453
  const ttlWarning = maybeBuildCacheTtlWarning({
2787
3454
  runtime,
2788
3455
  sessionId: effectiveSessionId,
@@ -2813,7 +3480,14 @@ export function createGatewayServer(deps = {}) {
2813
3480
  }
2814
3481
  let worktreeResolution = {};
2815
3482
  try {
2816
- worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
3483
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3484
+ provider: "claude",
3485
+ workspace,
3486
+ worktree,
3487
+ sessionId: effectiveSessionId,
3488
+ runtime,
3489
+ addDir,
3490
+ });
2817
3491
  }
2818
3492
  catch (err) {
2819
3493
  return createErrorResponse("claude_request", 1, "", corrId, err);
@@ -3024,6 +3698,7 @@ export function createGatewayServer(deps = {}) {
3024
3698
  .array(z.string())
3025
3699
  .optional()
3026
3700
  .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
3701
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3027
3702
  worktree: WORKTREE_SCHEMA.optional(),
3028
3703
  }, {
3029
3704
  title: "Codex request",
@@ -3031,7 +3706,7 @@ export function createGatewayServer(deps = {}) {
3031
3706
  destructiveHint: true,
3032
3707
  idempotentHint: false,
3033
3708
  openWorldHint: true,
3034
- }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
3709
+ }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, workspace, worktree, }) => {
3035
3710
  const startTime = Date.now();
3036
3711
  const prep = prepareCodexRequest({
3037
3712
  prompt,
@@ -3068,6 +3743,12 @@ export function createGatewayServer(deps = {}) {
3068
3743
  const { corrId, args } = prep;
3069
3744
  let durationMs = 0;
3070
3745
  let wasSuccessful = false;
3746
+ try {
3747
+ await getExistingSessionForProvider(sessionManager, sessionId, "codex");
3748
+ }
3749
+ catch (err) {
3750
+ return createErrorResponse("codex_request", 1, "", corrId, err);
3751
+ }
3071
3752
  safeFlightStart({
3072
3753
  correlationId: corrId,
3073
3754
  cli: "codex",
@@ -3081,7 +3762,15 @@ export function createGatewayServer(deps = {}) {
3081
3762
  const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
3082
3763
  let worktreeResolution = {};
3083
3764
  try {
3084
- worktreeResolution = await resolveWorktreeForRequest(worktree, sessionId, runtime);
3765
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3766
+ provider: "codex",
3767
+ workspace,
3768
+ worktree,
3769
+ sessionId,
3770
+ runtime,
3771
+ workingDir,
3772
+ addDir,
3773
+ });
3085
3774
  }
3086
3775
  catch (err) {
3087
3776
  return createErrorResponse("codex_request", 1, "", corrId, err);
@@ -3203,13 +3892,14 @@ export function createGatewayServer(deps = {}) {
3203
3892
  .max(3_600_000)
3204
3893
  .optional()
3205
3894
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
3895
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3206
3896
  }, {
3207
3897
  title: "Fork Codex session",
3208
3898
  readOnlyHint: false,
3209
3899
  destructiveHint: true,
3210
3900
  idempotentHint: false,
3211
3901
  openWorldHint: true,
3212
- }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, }) => {
3902
+ }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, workspace, }) => {
3213
3903
  const corrId = correlationId || randomUUID();
3214
3904
  const startTime = Date.now();
3215
3905
  let durationMs = 0;
@@ -3220,6 +3910,12 @@ export function createGatewayServer(deps = {}) {
3220
3910
  if (!sessionId && !forkLast) {
3221
3911
  return createErrorResponse("codex_fork_session", 1, "", corrId, new Error("one of sessionId or forkLast is required"));
3222
3912
  }
3913
+ try {
3914
+ await getExistingSessionForProvider(sessionManager, sessionId, "codex");
3915
+ }
3916
+ catch (err) {
3917
+ return createErrorResponse("codex_fork_session", 1, "", corrId, err);
3918
+ }
3223
3919
  let forkArgs;
3224
3920
  try {
3225
3921
  forkArgs = prepareCodexForkRequest({ prompt, sessionId, forkLast }).args;
@@ -3243,7 +3939,13 @@ export function createGatewayServer(deps = {}) {
3243
3939
  const finalArgs = [forkArgs[0], ...flagSegment, ...forkArgs.slice(1)];
3244
3940
  logger.info(`[${corrId}] codex_fork_session invoked (forkLast=${Boolean(forkLast)}, sessionId=${sessionId ? "set" : "unset"})`);
3245
3941
  try {
3246
- const result = await awaitJobOrDefer("codex", finalArgs, corrId, resolveIdleTimeout("codex", idleTimeoutMs), undefined, false, runtime);
3942
+ const worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3943
+ provider: "codex",
3944
+ workspace,
3945
+ sessionId,
3946
+ runtime,
3947
+ });
3948
+ const result = await awaitJobOrDefer("codex", finalArgs, corrId, resolveIdleTimeout("codex", idleTimeoutMs), undefined, false, runtime, undefined, undefined, undefined, undefined, undefined, worktreeResolution.cwd);
3247
3949
  if (isDeferredResponse(result)) {
3248
3950
  return buildDeferredToolResponse(result, sessionId);
3249
3951
  }
@@ -3334,6 +4036,7 @@ export function createGatewayServer(deps = {}) {
3334
4036
  .boolean()
3335
4037
  .optional()
3336
4038
  .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
4039
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3337
4040
  worktree: WORKTREE_SCHEMA.optional(),
3338
4041
  }, {
3339
4042
  title: "Gemini request",
@@ -3341,7 +4044,7 @@ export function createGatewayServer(deps = {}) {
3341
4044
  destructiveHint: true,
3342
4045
  idempotentHint: false,
3343
4046
  openWorldHint: true,
3344
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
4047
+ }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, workspace, worktree, }) => {
3345
4048
  return handleGeminiRequest({ sessionManager, logger, runtime }, {
3346
4049
  prompt,
3347
4050
  promptParts,
@@ -3367,6 +4070,7 @@ export function createGatewayServer(deps = {}) {
3367
4070
  attachments,
3368
4071
  skipTrust,
3369
4072
  yolo,
4073
+ workspace,
3370
4074
  worktree,
3371
4075
  });
3372
4076
  });
@@ -3544,6 +4248,7 @@ export function createGatewayServer(deps = {}) {
3544
4248
  .union([z.boolean(), z.string().min(1)])
3545
4249
  .optional()
3546
4250
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
4251
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3547
4252
  worktree: WORKTREE_SCHEMA.optional(),
3548
4253
  }, {
3549
4254
  title: "Grok request",
@@ -3551,7 +4256,7 @@ export function createGatewayServer(deps = {}) {
3551
4256
  destructiveHint: true,
3552
4257
  idempotentHint: false,
3553
4258
  openWorldHint: true,
3554
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
4259
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, workspace, worktree, }) => {
3555
4260
  return handleGrokRequest({ sessionManager, logger, runtime }, {
3556
4261
  prompt,
3557
4262
  promptParts,
@@ -3602,6 +4307,7 @@ export function createGatewayServer(deps = {}) {
3602
4307
  restoreCode,
3603
4308
  leaderSocket,
3604
4309
  nativeWorktree,
4310
+ workspace,
3605
4311
  worktree,
3606
4312
  });
3607
4313
  });
@@ -3684,6 +4390,7 @@ export function createGatewayServer(deps = {}) {
3684
4390
  .array(z.string())
3685
4391
  .optional()
3686
4392
  .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
4393
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3687
4394
  worktree: WORKTREE_SCHEMA.optional(),
3688
4395
  }, {
3689
4396
  title: "Mistral Vibe request",
@@ -3691,7 +4398,7 @@ export function createGatewayServer(deps = {}) {
3691
4398
  destructiveHint: true,
3692
4399
  idempotentHint: false,
3693
4400
  openWorldHint: true,
3694
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
4401
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, workspace, worktree, }) => {
3695
4402
  return handleMistralRequest({ sessionManager, logger, runtime }, {
3696
4403
  prompt,
3697
4404
  promptParts,
@@ -3717,6 +4424,7 @@ export function createGatewayServer(deps = {}) {
3717
4424
  maxTokens,
3718
4425
  workingDir,
3719
4426
  addDir,
4427
+ workspace,
3720
4428
  worktree,
3721
4429
  });
3722
4430
  });
@@ -3829,6 +4537,7 @@ export function createGatewayServer(deps = {}) {
3829
4537
  .array(z.string())
3830
4538
  .optional()
3831
4539
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
4540
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3832
4541
  worktree: WORKTREE_SCHEMA.optional(),
3833
4542
  approvalStrategy: z
3834
4543
  .enum(["legacy", "mcp_managed"])
@@ -3865,7 +4574,7 @@ export function createGatewayServer(deps = {}) {
3865
4574
  destructiveHint: true,
3866
4575
  idempotentHint: false,
3867
4576
  openWorldHint: true,
3868
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
4577
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, workspace, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
3869
4578
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
3870
4579
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
3871
4580
  }
@@ -3916,6 +4625,7 @@ export function createGatewayServer(deps = {}) {
3916
4625
  if (!useContinue && effectiveSessionId && activeSession?.id === effectiveSessionId) {
3917
4626
  useContinue = true;
3918
4627
  }
4628
+ const existingSession = await getExistingSessionForProvider(sessionManager, effectiveSessionId, "claude");
3919
4629
  if (useContinue) {
3920
4630
  args.push("--continue");
3921
4631
  }
@@ -3924,7 +4634,6 @@ export function createGatewayServer(deps = {}) {
3924
4634
  await sessionManager.updateSessionUsage(effectiveSessionId);
3925
4635
  }
3926
4636
  if (effectiveSessionId) {
3927
- const existingSession = await sessionManager.getSession(effectiveSessionId);
3928
4637
  if (!existingSession) {
3929
4638
  await sessionManager.createSession("claude", "Claude Session", effectiveSessionId);
3930
4639
  }
@@ -3936,7 +4645,14 @@ export function createGatewayServer(deps = {}) {
3936
4645
  });
3937
4646
  let worktreeResolution = {};
3938
4647
  try {
3939
- worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
4648
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
4649
+ provider: "claude",
4650
+ workspace,
4651
+ worktree,
4652
+ sessionId: effectiveSessionId,
4653
+ runtime,
4654
+ addDir,
4655
+ });
3940
4656
  }
3941
4657
  catch (err) {
3942
4658
  return createErrorResponse("claude_request_async", 1, "", corrId, err);
@@ -4076,6 +4792,7 @@ export function createGatewayServer(deps = {}) {
4076
4792
  .array(z.string())
4077
4793
  .optional()
4078
4794
  .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
4795
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4079
4796
  worktree: WORKTREE_SCHEMA.optional(),
4080
4797
  }, {
4081
4798
  title: "Codex request (async job)",
@@ -4083,7 +4800,7 @@ export function createGatewayServer(deps = {}) {
4083
4800
  destructiveHint: true,
4084
4801
  idempotentHint: false,
4085
4802
  openWorldHint: true,
4086
- }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
4803
+ }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, workspace, worktree, }) => {
4087
4804
  return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4088
4805
  prompt,
4089
4806
  promptParts,
@@ -4114,6 +4831,7 @@ export function createGatewayServer(deps = {}) {
4114
4831
  ignoreRules,
4115
4832
  workingDir,
4116
4833
  addDir,
4834
+ workspace,
4117
4835
  worktree,
4118
4836
  });
4119
4837
  });
@@ -4185,6 +4903,7 @@ export function createGatewayServer(deps = {}) {
4185
4903
  .boolean()
4186
4904
  .optional()
4187
4905
  .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
4906
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4188
4907
  worktree: WORKTREE_SCHEMA.optional(),
4189
4908
  }, {
4190
4909
  title: "Gemini request (async job)",
@@ -4192,7 +4911,7 @@ export function createGatewayServer(deps = {}) {
4192
4911
  destructiveHint: true,
4193
4912
  idempotentHint: false,
4194
4913
  openWorldHint: true,
4195
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
4914
+ }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, workspace, worktree, }) => {
4196
4915
  return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4197
4916
  prompt,
4198
4917
  promptParts,
@@ -4217,6 +4936,7 @@ export function createGatewayServer(deps = {}) {
4217
4936
  attachments,
4218
4937
  skipTrust,
4219
4938
  yolo,
4939
+ workspace,
4220
4940
  worktree,
4221
4941
  });
4222
4942
  });
@@ -4396,6 +5116,7 @@ export function createGatewayServer(deps = {}) {
4396
5116
  .union([z.boolean(), z.string().min(1)])
4397
5117
  .optional()
4398
5118
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
5119
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4399
5120
  worktree: WORKTREE_SCHEMA.optional(),
4400
5121
  }, {
4401
5122
  title: "Grok request (async job)",
@@ -4403,7 +5124,7 @@ export function createGatewayServer(deps = {}) {
4403
5124
  destructiveHint: true,
4404
5125
  idempotentHint: false,
4405
5126
  openWorldHint: true,
4406
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
5127
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, workspace, worktree, }) => {
4407
5128
  return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4408
5129
  prompt,
4409
5130
  promptParts,
@@ -4453,6 +5174,7 @@ export function createGatewayServer(deps = {}) {
4453
5174
  restoreCode,
4454
5175
  leaderSocket,
4455
5176
  nativeWorktree,
5177
+ workspace,
4456
5178
  worktree,
4457
5179
  });
4458
5180
  });
@@ -4534,6 +5256,7 @@ export function createGatewayServer(deps = {}) {
4534
5256
  .array(z.string())
4535
5257
  .optional()
4536
5258
  .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
5259
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4537
5260
  worktree: WORKTREE_SCHEMA.optional(),
4538
5261
  }, {
4539
5262
  title: "Mistral Vibe request (async job)",
@@ -4541,7 +5264,7 @@ export function createGatewayServer(deps = {}) {
4541
5264
  destructiveHint: true,
4542
5265
  idempotentHint: false,
4543
5266
  openWorldHint: true,
4544
- }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
5267
+ }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, workspace, worktree, }) => {
4545
5268
  return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4546
5269
  prompt,
4547
5270
  promptParts,
@@ -4566,6 +5289,7 @@ export function createGatewayServer(deps = {}) {
4566
5289
  maxTokens,
4567
5290
  workingDir,
4568
5291
  addDir,
5292
+ workspace,
4569
5293
  worktree,
4570
5294
  });
4571
5295
  });
@@ -4777,11 +5501,33 @@ export function createGatewayServer(deps = {}) {
4777
5501
  ? null
4778
5502
  : "Async job persistence is disabled (backend = 'none'). *_request_async tools are NOT registered on this gateway. Set [persistence].backend = 'sqlite' (or 'memory' + acknowledgeEphemeral = true) to enable them.",
4779
5503
  };
5504
+ const outboundProviders = {
5505
+ xai: providers.xai
5506
+ ? {
5507
+ configured: true,
5508
+ enabled: isXaiProviderEnabled(providers),
5509
+ apiKeyEnv: providers.xai.apiKeyEnv,
5510
+ apiKeyPresent: isXaiProviderEnabled(providers),
5511
+ baseUrl: providers.xai.baseUrl,
5512
+ defaultModel: providers.xai.defaultModel,
5513
+ mode: isXaiProviderEnabled(providers) ? "sync" : "configured-missing-key",
5514
+ }
5515
+ : {
5516
+ configured: false,
5517
+ enabled: false,
5518
+ apiKeyEnv: null,
5519
+ apiKeyPresent: false,
5520
+ baseUrl: null,
5521
+ defaultModel: null,
5522
+ mode: "disabled",
5523
+ },
5524
+ sources: providers.sources,
5525
+ };
4780
5526
  return {
4781
5527
  content: [
4782
5528
  {
4783
5529
  type: "text",
4784
- text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock }, null, 2),
5530
+ text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock, outboundProviders }, null, 2),
4785
5531
  },
4786
5532
  ],
4787
5533
  };
@@ -4850,7 +5596,7 @@ export function createGatewayServer(deps = {}) {
4850
5596
  });
4851
5597
  server.tool("upstream_contracts", "Return the gateway's declared provider CLI contracts; with probeInstalled true, diff against installed --help surfaces to detect flag drift.", {
4852
5598
  cli: z
4853
- .preprocess(value => (value === "" || value === null ? undefined : value), SESSION_PROVIDER_ENUM.optional())
5599
+ .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
4854
5600
  .describe("CLI filter (claude|codex|gemini|grok|mistral)"),
4855
5601
  probeInstalled: z
4856
5602
  .boolean()
@@ -4866,6 +5612,133 @@ export function createGatewayServer(deps = {}) {
4866
5612
  const report = buildUpstreamContractReport({ cli, probeInstalled });
4867
5613
  return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
4868
5614
  });
5615
+ server.tool("provider_subcommands_list", "Return a compact, filterable read-only catalog of declared provider CLI subcommands without flags or raw help.", {
5616
+ provider: z
5617
+ .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
5618
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral)"),
5619
+ tier: z
5620
+ .enum(["catalog", "inspect", "execute_candidate", "diagnostic"])
5621
+ .optional()
5622
+ .describe("Optional subcommand tier filter"),
5623
+ risk: z
5624
+ .enum([
5625
+ "read_only",
5626
+ "writes_local_config",
5627
+ "auth",
5628
+ "network",
5629
+ "starts_server",
5630
+ "updates_binary",
5631
+ "destructive",
5632
+ "executes_agent",
5633
+ ])
5634
+ .optional()
5635
+ .describe("Optional risk classification filter"),
5636
+ exposure: z
5637
+ .enum(["tracked_only", "mcp_readonly", "mcp_requires_approval", "not_exposed"])
5638
+ .optional()
5639
+ .describe("Optional MCP exposure filter"),
5640
+ commandPathPrefix: z
5641
+ .array(z.string().min(1))
5642
+ .optional()
5643
+ .describe("Optional command path prefix filter, e.g. ['agent']"),
5644
+ }, {
5645
+ title: "Provider subcommands catalog",
5646
+ readOnlyHint: true,
5647
+ destructiveHint: false,
5648
+ idempotentHint: true,
5649
+ openWorldHint: false,
5650
+ }, async ({ provider, tier, risk, exposure, commandPathPrefix }) => {
5651
+ const catalog = buildProviderSubcommandsCompactCatalog({
5652
+ provider,
5653
+ tier,
5654
+ risk,
5655
+ exposure,
5656
+ commandPathPrefix,
5657
+ });
5658
+ return {
5659
+ content: [
5660
+ {
5661
+ type: "text",
5662
+ text: JSON.stringify({ ...catalog, total: catalog.rows.length }),
5663
+ },
5664
+ ],
5665
+ };
5666
+ });
5667
+ server.tool("provider_subcommand_contract", "Return the detailed read-only contract for exactly one declared provider CLI subcommand.", {
5668
+ provider: CLI_TYPE_ENUM.describe("Provider (claude|codex|gemini|grok|mistral)"),
5669
+ commandPath: z.array(z.string().min(1)).min(1).describe("Command path segments"),
5670
+ }, {
5671
+ title: "Provider subcommand contract",
5672
+ readOnlyHint: true,
5673
+ destructiveHint: false,
5674
+ idempotentHint: true,
5675
+ openWorldHint: false,
5676
+ }, async ({ provider, commandPath }) => {
5677
+ const contract = getCliSubcommandContract(provider, commandPath);
5678
+ const payload = contract
5679
+ ? {
5680
+ schemaVersion: "provider-subcommand-contract.v1",
5681
+ contract: serializeCliSubcommandContract(provider, contract),
5682
+ }
5683
+ : {
5684
+ schemaVersion: "provider-subcommand-contract.v1",
5685
+ error: `No declared ${provider} subcommand contract for ${commandPath.join(" ")}`,
5686
+ };
5687
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
5688
+ });
5689
+ server.tool("provider_subcommand_drift", "Probe declared provider subcommand --help surfaces and return compact drift rows without raw help output.", {
5690
+ provider: z
5691
+ .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
5692
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral)"),
5693
+ includeClean: z
5694
+ .boolean()
5695
+ .default(false)
5696
+ .describe("When false, return only unavailable or drifted command paths"),
5697
+ }, {
5698
+ title: "Provider subcommand drift",
5699
+ readOnlyHint: true,
5700
+ destructiveHint: false,
5701
+ idempotentHint: true,
5702
+ openWorldHint: false,
5703
+ }, async ({ provider, includeClean }) => {
5704
+ const providers = provider ? [provider] : CLI_TYPES;
5705
+ const rows = providers.flatMap(cli => {
5706
+ const probe = probeInstalledCliContract(cli);
5707
+ return Object.values(probe.subcommands).flatMap(sub => {
5708
+ const drifted = !sub.available || sub.extraFlags.length > 0 || sub.missingFlags.length > 0;
5709
+ if (!includeClean && !drifted)
5710
+ return [];
5711
+ return [
5712
+ {
5713
+ provider: cli,
5714
+ commandPath: sub.commandPath,
5715
+ driftStatus: drifted ? "drift" : "clean",
5716
+ available: sub.available,
5717
+ extraVsContract: sub.extraFlags,
5718
+ missingFromBinary: sub.missingFlags,
5719
+ helpHash: sub.helpHash ?? null,
5720
+ risk: sub.risk,
5721
+ exposure: sub.exposure,
5722
+ tier: sub.tier,
5723
+ summary: sub.summary,
5724
+ warnings: sub.warnings,
5725
+ },
5726
+ ];
5727
+ });
5728
+ });
5729
+ return {
5730
+ content: [
5731
+ {
5732
+ type: "text",
5733
+ text: JSON.stringify({
5734
+ schemaVersion: "provider-subcommand-drift.v1",
5735
+ total: rows.length,
5736
+ rows,
5737
+ }),
5738
+ },
5739
+ ],
5740
+ };
5741
+ });
4869
5742
  server.tool("cli_upgrade", "Plan (dryRun, default true) or execute an upgrade for one provider CLI using its native update mechanism.", {
4870
5743
  cli: z.enum(["claude", "codex", "gemini", "grok", "mistral"]).describe("CLI to upgrade"),
4871
5744
  target: z
@@ -4921,8 +5794,8 @@ export function createGatewayServer(deps = {}) {
4921
5794
  };
4922
5795
  }
4923
5796
  });
4924
- server.tool("session_create", "Create a gateway session record for a provider CLI. NOTE: this is gateway bookkeeping (gw-* ID), not a provider-native session — Codex resume needs a real Codex UUID.", {
4925
- cli: SESSION_PROVIDER_ENUM.describe("CLI type (claude|codex|gemini|grok|mistral)"),
5797
+ server.tool("session_create", "Create a gateway session record for a provider. NOTE: this is gateway bookkeeping (gw-* ID), not a provider-native session — Codex resume needs a real Codex UUID.", {
5798
+ cli: SESSION_PROVIDER_ENUM.describe("Provider type (claude|codex|gemini|grok|mistral|grok-api)"),
4926
5799
  description: z.string().optional().describe("Session description"),
4927
5800
  setAsActive: z.boolean().default(true).describe("Set as active session"),
4928
5801
  }, {
@@ -4960,8 +5833,8 @@ export function createGatewayServer(deps = {}) {
4960
5833
  return createErrorResponse("session_create", 1, "", undefined, error);
4961
5834
  }
4962
5835
  });
4963
- server.tool("session_list", "List gateway session records and the active session per CLI, optionally filtered by CLI.", {
4964
- cli: SESSION_PROVIDER_ENUM.optional().describe("CLI filter (claude|codex|gemini|grok|mistral)"),
5836
+ server.tool("session_list", "List gateway session records and the active session per provider, optionally filtered by provider.", {
5837
+ cli: SESSION_PROVIDER_ENUM.optional().describe("Provider filter (claude|codex|gemini|grok|mistral|grok-api)"),
4965
5838
  }, {
4966
5839
  title: "List sessions",
4967
5840
  readOnlyHint: true,
@@ -4971,13 +5844,10 @@ export function createGatewayServer(deps = {}) {
4971
5844
  }, async ({ cli }) => {
4972
5845
  try {
4973
5846
  const sessions = await sessionManager.listSessions(cli);
4974
- const activeSessions = {
4975
- claude: await sessionManager.getActiveSession("claude"),
4976
- codex: await sessionManager.getActiveSession("codex"),
4977
- gemini: await sessionManager.getActiveSession("gemini"),
4978
- grok: await sessionManager.getActiveSession("grok"),
4979
- mistral: await sessionManager.getActiveSession("mistral"),
4980
- };
5847
+ const activeSessions = Object.fromEntries(await Promise.all(SESSION_PROVIDER_VALUES.map(async (provider) => [
5848
+ provider,
5849
+ await sessionManager.getActiveSession(provider),
5850
+ ])));
4981
5851
  const sessionList = sessions.map(s => ({
4982
5852
  id: s.id,
4983
5853
  cli: s.cli,
@@ -4993,13 +5863,10 @@ export function createGatewayServer(deps = {}) {
4993
5863
  text: JSON.stringify({
4994
5864
  total: sessionList.length,
4995
5865
  sessions: sessionList,
4996
- activeSessions: {
4997
- claude: activeSessions.claude?.id || null,
4998
- codex: activeSessions.codex?.id || null,
4999
- gemini: activeSessions.gemini?.id || null,
5000
- grok: activeSessions.grok?.id || null,
5001
- mistral: activeSessions.mistral?.id || null,
5002
- },
5866
+ activeSessions: Object.fromEntries(SESSION_PROVIDER_VALUES.map(provider => [
5867
+ provider,
5868
+ activeSessions[provider]?.id || null,
5869
+ ])),
5003
5870
  }, null, 2),
5004
5871
  },
5005
5872
  ],
@@ -5009,8 +5876,8 @@ export function createGatewayServer(deps = {}) {
5009
5876
  return createErrorResponse("session_list", 1, "", undefined, error);
5010
5877
  }
5011
5878
  });
5012
- server.tool("session_set_active", "Set or clear the active session for a CLI; the active session is used when a request omits sessionId.", {
5013
- cli: SESSION_PROVIDER_ENUM.describe("CLI type (claude|codex|gemini|grok|mistral)"),
5879
+ server.tool("session_set_active", "Set or clear the active session for a provider; the active session is used when a request omits sessionId.", {
5880
+ cli: SESSION_PROVIDER_ENUM.describe("Provider type (claude|codex|gemini|grok|mistral|grok-api)"),
5014
5881
  sessionId: z.string().nullable().describe("Session ID (null to clear)"),
5015
5882
  }, {
5016
5883
  title: "Set active session",
@@ -5028,7 +5895,7 @@ export function createGatewayServer(deps = {}) {
5028
5895
  type: "text",
5029
5896
  text: JSON.stringify({
5030
5897
  success: false,
5031
- error: "Session not found or does not belong to the specified CLI",
5898
+ error: "Session not found or does not belong to the specified provider",
5032
5899
  }, null, 2),
5033
5900
  },
5034
5901
  ],
@@ -5169,8 +6036,8 @@ export function createGatewayServer(deps = {}) {
5169
6036
  return createErrorResponse("session_get", 1, "", undefined, error);
5170
6037
  }
5171
6038
  });
5172
- server.tool("session_clear_all", "Delete all gateway session records, optionally scoped to one CLI.", {
5173
- cli: SESSION_PROVIDER_ENUM.optional().describe("CLI filter (claude|codex|gemini|grok|mistral)"),
6039
+ server.tool("session_clear_all", "Delete all gateway session records, optionally scoped to one provider.", {
6040
+ cli: SESSION_PROVIDER_ENUM.optional().describe("Provider filter (claude|codex|gemini|grok|mistral|grok-api)"),
5174
6041
  }, {
5175
6042
  title: "Clear sessions",
5176
6043
  readOnlyHint: false,
@@ -5289,6 +6156,210 @@ async function shutdown(signal) {
5289
6156
  }
5290
6157
  process.on("SIGTERM", () => shutdown("SIGTERM"));
5291
6158
  process.on("SIGINT", () => shutdown("SIGINT"));
6159
+ function readMutableGatewayConfig(configPath = defaultGatewayConfigPath()) {
6160
+ if (!existsSync(configPath))
6161
+ return {};
6162
+ const require = createRequire(import.meta.url);
6163
+ const TOML = require("smol-toml");
6164
+ return TOML.parse(readFileSync(configPath, "utf8"));
6165
+ }
6166
+ function writeMutableGatewayConfig(data, configPath = defaultGatewayConfigPath()) {
6167
+ const require = createRequire(import.meta.url);
6168
+ const TOML = require("smol-toml");
6169
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
6170
+ writeFileSync(configPath, TOML.stringify(data), { mode: 0o600 });
6171
+ chmodSync(configPath, 0o600);
6172
+ }
6173
+ function ensureOAuthTable(config) {
6174
+ config.http ??= {};
6175
+ config.http.oauth ??= {};
6176
+ const oauth = config.http.oauth;
6177
+ oauth.enabled ??= true;
6178
+ oauth.issuer ??= "auto";
6179
+ oauth.require_pkce ??= true;
6180
+ oauth.registration_policy ??= "static_clients";
6181
+ oauth.allow_public_clients ??= false;
6182
+ oauth.token_ttl_seconds ??= 3600;
6183
+ oauth.clients ??= [];
6184
+ return oauth;
6185
+ }
6186
+ function argValue(args, name) {
6187
+ const idx = args.indexOf(name);
6188
+ return idx >= 0 ? args[idx + 1] : undefined;
6189
+ }
6190
+ function requireArg(args, name) {
6191
+ const value = argValue(args, name);
6192
+ if (!value)
6193
+ throw new Error(`Missing ${name}`);
6194
+ return value;
6195
+ }
6196
+ function localBaseUrlForPrint() {
6197
+ const publicUrl = process.env.LLM_GATEWAY_PUBLIC_URL;
6198
+ if (publicUrl) {
6199
+ try {
6200
+ return new URL(publicUrl).origin;
6201
+ }
6202
+ catch {
6203
+ }
6204
+ }
6205
+ return `http://${process.env.LLM_GATEWAY_HTTP_HOST ?? "127.0.0.1"}:${process.env.LLM_GATEWAY_HTTP_PORT ?? "3333"}`;
6206
+ }
6207
+ function printJsonLine(value) {
6208
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
6209
+ }
6210
+ function runOAuthCommand(args) {
6211
+ const [scope, action] = args;
6212
+ const config = readMutableGatewayConfig();
6213
+ const oauth = ensureOAuthTable(config);
6214
+ if (scope === "client") {
6215
+ const clients = (Array.isArray(oauth.clients) ? oauth.clients : []);
6216
+ oauth.clients = clients;
6217
+ if (action === "add") {
6218
+ const clientId = args[2];
6219
+ if (!clientId)
6220
+ throw new Error("Usage: llm-cli-gateway oauth client add <client-id> --redirect-uri <uri> [--print-once]");
6221
+ const redirectUri = requireArg(args, "--redirect-uri");
6222
+ const secret = generateSecret();
6223
+ clients.push({
6224
+ client_id: clientId,
6225
+ client_secret_hash: hashSecret(secret),
6226
+ allowed_redirect_uris: [redirectUri],
6227
+ scopes: ["mcp"],
6228
+ });
6229
+ writeMutableGatewayConfig(config);
6230
+ printJsonLine({
6231
+ ok: true,
6232
+ client_id: clientId,
6233
+ ...(args.includes("--print-once") ? { client_secret: secret } : {}),
6234
+ oauth: {
6235
+ issuer: localBaseUrlForPrint(),
6236
+ authorization_url: `${localBaseUrlForPrint()}/oauth/authorize`,
6237
+ token_url: `${localBaseUrlForPrint()}/oauth/token`,
6238
+ },
6239
+ note: args.includes("--print-once")
6240
+ ? "client_secret is shown once; it is stored only as a hash."
6241
+ : "client secret generated and stored only as a hash; rerun rotate --print-once if needed.",
6242
+ });
6243
+ return;
6244
+ }
6245
+ if (action === "list") {
6246
+ printJsonLine({
6247
+ ok: true,
6248
+ clients: clients.map(client => ({
6249
+ client_id: client.client_id,
6250
+ redirect_uris: client.allowed_redirect_uris ?? [],
6251
+ secret_configured: Boolean(client.client_secret_hash),
6252
+ })),
6253
+ });
6254
+ return;
6255
+ }
6256
+ if (action === "rotate") {
6257
+ const clientId = args[2];
6258
+ const client = clients.find(candidate => candidate.client_id === clientId);
6259
+ if (!client)
6260
+ throw new Error(`Unknown OAuth client ${clientId}`);
6261
+ const secret = generateSecret();
6262
+ client.client_secret_hash = hashSecret(secret);
6263
+ writeMutableGatewayConfig(config);
6264
+ printJsonLine({
6265
+ ok: true,
6266
+ client_id: clientId,
6267
+ ...(args.includes("--print-once") ? { client_secret: secret } : {}),
6268
+ note: "Future OAuth exchanges use the rotated secret; already-issued opaque access tokens expire by token TTL or server restart.",
6269
+ });
6270
+ return;
6271
+ }
6272
+ if (action === "revoke") {
6273
+ const clientId = args[2];
6274
+ oauth.clients = clients.filter(client => client.client_id !== clientId);
6275
+ writeMutableGatewayConfig(config);
6276
+ printJsonLine({
6277
+ ok: true,
6278
+ client_id: clientId,
6279
+ note: "Future OAuth exchanges are revoked; already-issued opaque access tokens expire by token TTL or server restart.",
6280
+ });
6281
+ return;
6282
+ }
6283
+ }
6284
+ if (scope === "shared-secret") {
6285
+ if (action === "set" || action === "rotate") {
6286
+ const secret = generateSecret();
6287
+ oauth.registration_policy = "shared_secret";
6288
+ oauth.shared_secret = {
6289
+ enabled: true,
6290
+ secret_hash: hashSecret(secret),
6291
+ prompt_label: "Gateway access code",
6292
+ };
6293
+ writeMutableGatewayConfig(config);
6294
+ printJsonLine({
6295
+ ok: true,
6296
+ shared_secret_enabled: true,
6297
+ ...(args.includes("--print-once") ? { shared_secret: secret } : {}),
6298
+ note: args.includes("--print-once")
6299
+ ? "shared_secret is shown once; it is stored only as a hash."
6300
+ : "shared secret generated and stored only as a hash.",
6301
+ });
6302
+ return;
6303
+ }
6304
+ if (action === "disable") {
6305
+ oauth.shared_secret = { enabled: false, prompt_label: "Gateway access code" };
6306
+ if (oauth.registration_policy === "shared_secret")
6307
+ oauth.registration_policy = "static_clients";
6308
+ writeMutableGatewayConfig(config);
6309
+ printJsonLine({ ok: true, shared_secret_enabled: false });
6310
+ return;
6311
+ }
6312
+ }
6313
+ throw new Error("Usage: llm-cli-gateway oauth client|shared-secret ...");
6314
+ }
6315
+ function runWorkspaceCommand(args) {
6316
+ const [action] = args;
6317
+ if (action === "list") {
6318
+ const registry = loadWorkspaceRegistry(logger);
6319
+ printJsonLine({
6320
+ ok: true,
6321
+ default: registry.defaultAlias,
6322
+ workspaces: registry.repos.map(describeWorkspace),
6323
+ allowed_roots: registry.allowedRoots.map(root => ({
6324
+ alias: root.alias,
6325
+ path: root.path,
6326
+ allow_create_directories: root.allowCreateDirectories,
6327
+ allow_init_git_repos: root.allowInitGitRepos,
6328
+ })),
6329
+ });
6330
+ return;
6331
+ }
6332
+ if (action === "create") {
6333
+ const alias = args[1];
6334
+ if (!alias)
6335
+ throw new Error("Usage: llm-cli-gateway workspace create <alias> --root <root> --slug <slug> --kind folder|git [--default]");
6336
+ const repo = createWorkspace({
6337
+ alias,
6338
+ rootAlias: requireArg(args, "--root"),
6339
+ slug: requireArg(args, "--slug"),
6340
+ kind: (argValue(args, "--kind") ?? "git"),
6341
+ setDefault: args.includes("--default"),
6342
+ logger,
6343
+ });
6344
+ printJsonLine({ ok: true, workspace: describeWorkspace(repo) });
6345
+ return;
6346
+ }
6347
+ if (action === "add") {
6348
+ const alias = args[1];
6349
+ const repoPath = args[2];
6350
+ if (!alias || !repoPath)
6351
+ throw new Error("Usage: llm-cli-gateway workspace add <alias> <path> [--default]");
6352
+ const repo = registerExistingWorkspace({
6353
+ alias,
6354
+ repoPath,
6355
+ setDefault: args.includes("--default"),
6356
+ logger,
6357
+ });
6358
+ printJsonLine({ ok: true, workspace: describeWorkspace(repo) });
6359
+ return;
6360
+ }
6361
+ throw new Error("Usage: llm-cli-gateway workspace list|add|create ...");
6362
+ }
5292
6363
  async function main() {
5293
6364
  startWindowsBootstrapperSelfHeal();
5294
6365
  const args = process.argv.slice(2);
@@ -5302,6 +6373,8 @@ async function main() {
5302
6373
  "",
5303
6374
  "Usage:",
5304
6375
  " llm-cli-gateway [doctor --json|contracts --json|--transport=http|--version]",
6376
+ " llm-cli-gateway oauth client add <id> --redirect-uri <uri> [--print-once]",
6377
+ " llm-cli-gateway workspace list|add|create",
5305
6378
  "",
5306
6379
  "Doctor:",
5307
6380
  " doctor --json # environment, providers, declared contracts",
@@ -5323,12 +6396,18 @@ async function main() {
5323
6396
  process.stderr.write("Only doctor --json is supported in this layer.\n");
5324
6397
  process.exit(2);
5325
6398
  }
6399
+ if (args[0] === "oauth") {
6400
+ runOAuthCommand(args.slice(1));
6401
+ return;
6402
+ }
6403
+ if (args[0] === "workspace") {
6404
+ runWorkspaceCommand(args.slice(1));
6405
+ return;
6406
+ }
5326
6407
  if (args[0] === "contracts") {
5327
6408
  if (args.includes("--json")) {
5328
6409
  const cliArg = args.find(arg => arg.startsWith("--cli="))?.split("=")[1];
5329
- const cli = SESSION_PROVIDER_VALUES.includes(cliArg)
5330
- ? cliArg
5331
- : undefined;
6410
+ const cli = CLI_TYPES.includes(cliArg) ? cliArg : undefined;
5332
6411
  if (cliArg && !cli) {
5333
6412
  process.stderr.write(`Unsupported --cli value: ${cliArg}\n`);
5334
6413
  process.exit(2);