llm-cli-gateway 2.4.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";
@@ -17,7 +18,7 @@ import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-ma
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, loadProvidersConfig, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
21
+ import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, defaultGatewayConfigPath, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
21
22
  import { createXaiResponse, XaiApiError, } from "./xai-api-provider.js";
22
23
  import { checkHealth } from "./health.js";
23
24
  import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
@@ -32,7 +33,10 @@ import { resolvePromptInput, PromptPartsSchema, assembleClaudeCacheBlocks, } fro
32
33
  import { computeSessionCacheStats, computeTtlRemaining, readPersistedRequest, PERSISTED_REQUEST_DEFAULT_MAX_CHARS, } from "./cache-stats.js";
33
34
  import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
34
35
  import { startHttpGateway } from "./http-transport.js";
36
+ import { getRequestContext } from "./request-context.js";
35
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";
36
40
  import { registerValidationTools } from "./validation-tools.js";
37
41
  import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildProviderSubcommandsCompactCatalog, buildUpstreamContractReport, getCliSubcommandContract, probeInstalledCliContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
38
42
  import { entrypointFileURL } from "./entrypoint-url.js";
@@ -252,6 +256,12 @@ export const WORKTREE_SCHEMA = z
252
256
  "path. NOTE: callers should `.gitignore` the `.worktrees/` " +
253
257
  "directory in their repo (the gateway does NOT auto-gitignore — " +
254
258
  "see slice λ spec Q4).");
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.");
255
265
  export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
256
266
  export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
257
267
  let activeServer = null;
@@ -286,6 +296,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
286
296
  persistence: deps.persistence ?? getPersistenceConfig(runtimeLogger),
287
297
  cacheAwareness: deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger),
288
298
  providers: deps.providers ?? getProvidersConfig(runtimeLogger),
299
+ workspaces: deps.workspaces ?? loadWorkspaceRegistry(runtimeLogger),
289
300
  };
290
301
  }
291
302
  export function shouldRegisterGrokApiTools(providers) {
@@ -415,7 +426,7 @@ function buildDeferredToolResponse(deferred, sessionId) {
415
426
  ],
416
427
  };
417
428
  }
418
- export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime) {
429
+ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime, options = {}) {
419
430
  if (!worktreeOpt)
420
431
  return {};
421
432
  const sessionManager = runtime.sessionManager;
@@ -423,12 +434,21 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
423
434
  const session = await Promise.resolve(sessionManager.getSession(sessionId));
424
435
  const existingPath = session?.metadata?.worktreePath;
425
436
  if (typeof existingPath === "string" && existingPath.length > 0) {
426
- 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
+ };
427
447
  }
428
448
  }
429
449
  const name = worktreeOpt === true ? undefined : worktreeOpt.name;
430
450
  const ref = worktreeOpt === true ? undefined : worktreeOpt.ref;
431
- const repoRoot = process.cwd();
451
+ const repoRoot = options.repoRoot ?? process.cwd();
432
452
  const handle = await createWorktree({
433
453
  repoRoot,
434
454
  name,
@@ -439,13 +459,204 @@ export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime)
439
459
  await Promise.resolve(sessionManager.updateSessionMetadata(sessionId, {
440
460
  worktreePath: handle.path,
441
461
  worktreeName: handle.name,
462
+ ...(options.workspaceAlias ? { workspaceAlias: options.workspaceAlias } : {}),
463
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
464
+ }));
465
+ }
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,
442
520
  }));
443
521
  }
444
- return { cwd: handle.path, worktreePath: handle.path };
522
+ return { cwd: workspace?.cwd, workspace };
445
523
  }
446
524
  export function formatWorktreePrefix(worktreePath) {
447
525
  return worktreePath ? `[gateway] worktree=${worktreePath}\n` : "";
448
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
+ }
449
660
  function createErrorResponse(cli, code, stderr, correlationId, error) {
450
661
  let errorMessage = `Error executing ${cli} CLI`;
451
662
  const isLaunchExit = code === 127 || code === -4058;
@@ -2016,6 +2227,7 @@ function resolveHandlerRuntime(deps) {
2016
2227
  sessionManager: deps.sessionManager,
2017
2228
  logger: normalizedLogger,
2018
2229
  asyncJobManager: asyncDeps.asyncJobManager,
2230
+ workspaces: deps.workspaces,
2019
2231
  });
2020
2232
  }
2021
2233
  export async function handleGeminiRequest(deps, params) {
@@ -2071,13 +2283,20 @@ export async function handleGeminiRequest(deps, params) {
2071
2283
  args.push(...sessionPlan.args);
2072
2284
  let worktreeResolution = {};
2073
2285
  try {
2074
- 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
+ });
2075
2294
  }
2076
2295
  catch (err) {
2077
2296
  return createErrorResponse("gemini_request", 1, "", corrId, err);
2078
2297
  }
2079
2298
  const geminiFrHandoff = buildAsyncFlightRecorderHandoff("gemini", prep, params.sessionId, params.outputFormat);
2080
- 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);
2081
2300
  if (isDeferredResponse(result)) {
2082
2301
  return buildDeferredToolResponse(result, effectiveSessionIdHint);
2083
2302
  }
@@ -2209,7 +2428,14 @@ export async function handleGeminiRequestAsync(deps, params) {
2209
2428
  }
2210
2429
  let worktreeResolution = {};
2211
2430
  try {
2212
- 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
+ });
2213
2439
  }
2214
2440
  catch (err) {
2215
2441
  return createErrorResponse("gemini_request_async", 1, "", corrId, err);
@@ -2322,7 +2548,14 @@ export async function handleGrokRequest(deps, params) {
2322
2548
  args.push(...sessionResult.resumeArgs);
2323
2549
  let worktreeResolution = {};
2324
2550
  try {
2325
- 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
+ });
2326
2559
  }
2327
2560
  catch (err) {
2328
2561
  return createErrorResponse("grok_request", 1, "", corrId, err);
@@ -2490,7 +2723,14 @@ export async function handleGrokRequestAsync(deps, params) {
2490
2723
  }
2491
2724
  let worktreeResolution = {};
2492
2725
  try {
2493
- 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
+ });
2494
2734
  }
2495
2735
  catch (err) {
2496
2736
  return createErrorResponse("grok_request_async", 1, "", corrId, err);
@@ -2578,7 +2818,15 @@ export async function handleMistralRequest(deps, params) {
2578
2818
  args.push(...sessionResult.resumeArgs);
2579
2819
  let worktreeResolution = {};
2580
2820
  try {
2581
- 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
+ });
2582
2830
  }
2583
2831
  catch (err) {
2584
2832
  return createErrorResponse("mistral_request", 1, "", corrId, err);
@@ -2732,7 +2980,15 @@ export async function handleMistralRequestAsync(deps, params) {
2732
2980
  }
2733
2981
  let worktreeResolution = {};
2734
2982
  try {
2735
- 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
+ });
2736
2992
  }
2737
2993
  catch (err) {
2738
2994
  return createErrorResponse("mistral_request_async", 1, "", corrId, err);
@@ -2844,7 +3100,15 @@ export async function handleCodexRequestAsync(deps, params) {
2844
3100
  }
2845
3101
  let worktreeResolution = {};
2846
3102
  try {
2847
- 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
+ });
2848
3112
  }
2849
3113
  catch (err) {
2850
3114
  runPrepCleanupLocally();
@@ -2899,6 +3163,7 @@ export function createGatewayServer(deps = {}) {
2899
3163
  const server = newGatewayMcpServer(asyncJobsEnabled, grokApiToolsEnabled);
2900
3164
  registerBaseResources(server, runtime);
2901
3165
  registerValidationTools(server, { asyncJobManager });
3166
+ registerWorkspaceTools(server, runtime);
2902
3167
  if (grokApiToolsEnabled) {
2903
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.", {
2904
3169
  prompt: z
@@ -3082,6 +3347,7 @@ export function createGatewayServer(deps = {}) {
3082
3347
  .array(z.string())
3083
3348
  .optional()
3084
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(),
3085
3351
  worktree: WORKTREE_SCHEMA.optional(),
3086
3352
  approvalStrategy: z
3087
3353
  .enum(["legacy", "mcp_managed"])
@@ -3119,7 +3385,7 @@ export function createGatewayServer(deps = {}) {
3119
3385
  destructiveHint: true,
3120
3386
  idempotentHint: false,
3121
3387
  openWorldHint: true,
3122
- }, 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, }) => {
3123
3389
  const startTime = Date.now();
3124
3390
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
3125
3391
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
@@ -3214,7 +3480,14 @@ export function createGatewayServer(deps = {}) {
3214
3480
  }
3215
3481
  let worktreeResolution = {};
3216
3482
  try {
3217
- 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
+ });
3218
3491
  }
3219
3492
  catch (err) {
3220
3493
  return createErrorResponse("claude_request", 1, "", corrId, err);
@@ -3425,6 +3698,7 @@ export function createGatewayServer(deps = {}) {
3425
3698
  .array(z.string())
3426
3699
  .optional()
3427
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(),
3428
3702
  worktree: WORKTREE_SCHEMA.optional(),
3429
3703
  }, {
3430
3704
  title: "Codex request",
@@ -3432,7 +3706,7 @@ export function createGatewayServer(deps = {}) {
3432
3706
  destructiveHint: true,
3433
3707
  idempotentHint: false,
3434
3708
  openWorldHint: true,
3435
- }, 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, }) => {
3436
3710
  const startTime = Date.now();
3437
3711
  const prep = prepareCodexRequest({
3438
3712
  prompt,
@@ -3488,7 +3762,15 @@ export function createGatewayServer(deps = {}) {
3488
3762
  const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
3489
3763
  let worktreeResolution = {};
3490
3764
  try {
3491
- 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
+ });
3492
3774
  }
3493
3775
  catch (err) {
3494
3776
  return createErrorResponse("codex_request", 1, "", corrId, err);
@@ -3610,13 +3892,14 @@ export function createGatewayServer(deps = {}) {
3610
3892
  .max(3_600_000)
3611
3893
  .optional()
3612
3894
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
3895
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3613
3896
  }, {
3614
3897
  title: "Fork Codex session",
3615
3898
  readOnlyHint: false,
3616
3899
  destructiveHint: true,
3617
3900
  idempotentHint: false,
3618
3901
  openWorldHint: true,
3619
- }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, }) => {
3902
+ }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, workspace, }) => {
3620
3903
  const corrId = correlationId || randomUUID();
3621
3904
  const startTime = Date.now();
3622
3905
  let durationMs = 0;
@@ -3656,7 +3939,13 @@ export function createGatewayServer(deps = {}) {
3656
3939
  const finalArgs = [forkArgs[0], ...flagSegment, ...forkArgs.slice(1)];
3657
3940
  logger.info(`[${corrId}] codex_fork_session invoked (forkLast=${Boolean(forkLast)}, sessionId=${sessionId ? "set" : "unset"})`);
3658
3941
  try {
3659
- 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);
3660
3949
  if (isDeferredResponse(result)) {
3661
3950
  return buildDeferredToolResponse(result, sessionId);
3662
3951
  }
@@ -3747,6 +4036,7 @@ export function createGatewayServer(deps = {}) {
3747
4036
  .boolean()
3748
4037
  .optional()
3749
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(),
3750
4040
  worktree: WORKTREE_SCHEMA.optional(),
3751
4041
  }, {
3752
4042
  title: "Gemini request",
@@ -3754,7 +4044,7 @@ export function createGatewayServer(deps = {}) {
3754
4044
  destructiveHint: true,
3755
4045
  idempotentHint: false,
3756
4046
  openWorldHint: true,
3757
- }, 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, }) => {
3758
4048
  return handleGeminiRequest({ sessionManager, logger, runtime }, {
3759
4049
  prompt,
3760
4050
  promptParts,
@@ -3780,6 +4070,7 @@ export function createGatewayServer(deps = {}) {
3780
4070
  attachments,
3781
4071
  skipTrust,
3782
4072
  yolo,
4073
+ workspace,
3783
4074
  worktree,
3784
4075
  });
3785
4076
  });
@@ -3957,6 +4248,7 @@ export function createGatewayServer(deps = {}) {
3957
4248
  .union([z.boolean(), z.string().min(1)])
3958
4249
  .optional()
3959
4250
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
4251
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
3960
4252
  worktree: WORKTREE_SCHEMA.optional(),
3961
4253
  }, {
3962
4254
  title: "Grok request",
@@ -3964,7 +4256,7 @@ export function createGatewayServer(deps = {}) {
3964
4256
  destructiveHint: true,
3965
4257
  idempotentHint: false,
3966
4258
  openWorldHint: true,
3967
- }, 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, }) => {
3968
4260
  return handleGrokRequest({ sessionManager, logger, runtime }, {
3969
4261
  prompt,
3970
4262
  promptParts,
@@ -4015,6 +4307,7 @@ export function createGatewayServer(deps = {}) {
4015
4307
  restoreCode,
4016
4308
  leaderSocket,
4017
4309
  nativeWorktree,
4310
+ workspace,
4018
4311
  worktree,
4019
4312
  });
4020
4313
  });
@@ -4097,6 +4390,7 @@ export function createGatewayServer(deps = {}) {
4097
4390
  .array(z.string())
4098
4391
  .optional()
4099
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(),
4100
4394
  worktree: WORKTREE_SCHEMA.optional(),
4101
4395
  }, {
4102
4396
  title: "Mistral Vibe request",
@@ -4104,7 +4398,7 @@ export function createGatewayServer(deps = {}) {
4104
4398
  destructiveHint: true,
4105
4399
  idempotentHint: false,
4106
4400
  openWorldHint: true,
4107
- }, 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, }) => {
4108
4402
  return handleMistralRequest({ sessionManager, logger, runtime }, {
4109
4403
  prompt,
4110
4404
  promptParts,
@@ -4130,6 +4424,7 @@ export function createGatewayServer(deps = {}) {
4130
4424
  maxTokens,
4131
4425
  workingDir,
4132
4426
  addDir,
4427
+ workspace,
4133
4428
  worktree,
4134
4429
  });
4135
4430
  });
@@ -4242,6 +4537,7 @@ export function createGatewayServer(deps = {}) {
4242
4537
  .array(z.string())
4243
4538
  .optional()
4244
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(),
4245
4541
  worktree: WORKTREE_SCHEMA.optional(),
4246
4542
  approvalStrategy: z
4247
4543
  .enum(["legacy", "mcp_managed"])
@@ -4278,7 +4574,7 @@ export function createGatewayServer(deps = {}) {
4278
4574
  destructiveHint: true,
4279
4575
  idempotentHint: false,
4280
4576
  openWorldHint: true,
4281
- }, 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, }) => {
4282
4578
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
4283
4579
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
4284
4580
  }
@@ -4349,7 +4645,14 @@ export function createGatewayServer(deps = {}) {
4349
4645
  });
4350
4646
  let worktreeResolution = {};
4351
4647
  try {
4352
- 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
+ });
4353
4656
  }
4354
4657
  catch (err) {
4355
4658
  return createErrorResponse("claude_request_async", 1, "", corrId, err);
@@ -4489,6 +4792,7 @@ export function createGatewayServer(deps = {}) {
4489
4792
  .array(z.string())
4490
4793
  .optional()
4491
4794
  .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
4795
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4492
4796
  worktree: WORKTREE_SCHEMA.optional(),
4493
4797
  }, {
4494
4798
  title: "Codex request (async job)",
@@ -4496,7 +4800,7 @@ export function createGatewayServer(deps = {}) {
4496
4800
  destructiveHint: true,
4497
4801
  idempotentHint: false,
4498
4802
  openWorldHint: true,
4499
- }, 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, }) => {
4500
4804
  return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4501
4805
  prompt,
4502
4806
  promptParts,
@@ -4527,6 +4831,7 @@ export function createGatewayServer(deps = {}) {
4527
4831
  ignoreRules,
4528
4832
  workingDir,
4529
4833
  addDir,
4834
+ workspace,
4530
4835
  worktree,
4531
4836
  });
4532
4837
  });
@@ -4598,6 +4903,7 @@ export function createGatewayServer(deps = {}) {
4598
4903
  .boolean()
4599
4904
  .optional()
4600
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(),
4601
4907
  worktree: WORKTREE_SCHEMA.optional(),
4602
4908
  }, {
4603
4909
  title: "Gemini request (async job)",
@@ -4605,7 +4911,7 @@ export function createGatewayServer(deps = {}) {
4605
4911
  destructiveHint: true,
4606
4912
  idempotentHint: false,
4607
4913
  openWorldHint: true,
4608
- }, 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, }) => {
4609
4915
  return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4610
4916
  prompt,
4611
4917
  promptParts,
@@ -4630,6 +4936,7 @@ export function createGatewayServer(deps = {}) {
4630
4936
  attachments,
4631
4937
  skipTrust,
4632
4938
  yolo,
4939
+ workspace,
4633
4940
  worktree,
4634
4941
  });
4635
4942
  });
@@ -4809,6 +5116,7 @@ export function createGatewayServer(deps = {}) {
4809
5116
  .union([z.boolean(), z.string().min(1)])
4810
5117
  .optional()
4811
5118
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
5119
+ workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4812
5120
  worktree: WORKTREE_SCHEMA.optional(),
4813
5121
  }, {
4814
5122
  title: "Grok request (async job)",
@@ -4816,7 +5124,7 @@ export function createGatewayServer(deps = {}) {
4816
5124
  destructiveHint: true,
4817
5125
  idempotentHint: false,
4818
5126
  openWorldHint: true,
4819
- }, 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, }) => {
4820
5128
  return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4821
5129
  prompt,
4822
5130
  promptParts,
@@ -4866,6 +5174,7 @@ export function createGatewayServer(deps = {}) {
4866
5174
  restoreCode,
4867
5175
  leaderSocket,
4868
5176
  nativeWorktree,
5177
+ workspace,
4869
5178
  worktree,
4870
5179
  });
4871
5180
  });
@@ -4947,6 +5256,7 @@ export function createGatewayServer(deps = {}) {
4947
5256
  .array(z.string())
4948
5257
  .optional()
4949
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(),
4950
5260
  worktree: WORKTREE_SCHEMA.optional(),
4951
5261
  }, {
4952
5262
  title: "Mistral Vibe request (async job)",
@@ -4954,7 +5264,7 @@ export function createGatewayServer(deps = {}) {
4954
5264
  destructiveHint: true,
4955
5265
  idempotentHint: false,
4956
5266
  openWorldHint: true,
4957
- }, 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, }) => {
4958
5268
  return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4959
5269
  prompt,
4960
5270
  promptParts,
@@ -4979,6 +5289,7 @@ export function createGatewayServer(deps = {}) {
4979
5289
  maxTokens,
4980
5290
  workingDir,
4981
5291
  addDir,
5292
+ workspace,
4982
5293
  worktree,
4983
5294
  });
4984
5295
  });
@@ -5845,6 +6156,210 @@ async function shutdown(signal) {
5845
6156
  }
5846
6157
  process.on("SIGTERM", () => shutdown("SIGTERM"));
5847
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
+ }
5848
6363
  async function main() {
5849
6364
  startWindowsBootstrapperSelfHeal();
5850
6365
  const args = process.argv.slice(2);
@@ -5858,6 +6373,8 @@ async function main() {
5858
6373
  "",
5859
6374
  "Usage:",
5860
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",
5861
6378
  "",
5862
6379
  "Doctor:",
5863
6380
  " doctor --json # environment, providers, declared contracts",
@@ -5879,6 +6396,14 @@ async function main() {
5879
6396
  process.stderr.write("Only doctor --json is supported in this layer.\n");
5880
6397
  process.exit(2);
5881
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
+ }
5882
6407
  if (args[0] === "contracts") {
5883
6408
  if (args.includes("--json")) {
5884
6409
  const cliArg = args.find(arg => arg.startsWith("--cli="))?.split("=")[1];