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/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/dist/auth.d.ts +44 -1
- package/dist/auth.js +60 -13
- package/dist/config.d.ts +2 -0
- package/dist/config.js +151 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +22 -11
- package/dist/http-transport.js +74 -12
- package/dist/index.d.ts +16 -1
- package/dist/index.js +554 -29
- package/dist/oauth.d.ts +38 -0
- package/dist/oauth.js +441 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +8 -0
- package/dist/upstream-contracts.js +42 -26
- package/dist/workspace-registry.d.ts +63 -0
- package/dist/workspace-registry.js +417 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +42 -1
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 {
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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];
|