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