llm-cli-gateway 2.7.0 → 2.9.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 +82 -0
- package/README.md +28 -1
- package/dist/acp/client.d.ts +78 -0
- package/dist/acp/client.js +201 -0
- package/dist/acp/errors.d.ts +63 -0
- package/dist/acp/errors.js +139 -0
- package/dist/acp/json-rpc-stdio.d.ts +71 -0
- package/dist/acp/json-rpc-stdio.js +375 -0
- package/dist/acp/process-manager.d.ts +66 -0
- package/dist/acp/process-manager.js +364 -0
- package/dist/acp/provider-registry.d.ts +24 -0
- package/dist/acp/provider-registry.js +82 -0
- package/dist/acp/types.d.ts +557 -0
- package/dist/acp/types.js +335 -0
- package/dist/approval-manager.d.ts +1 -0
- package/dist/approval-manager.js +14 -1
- package/dist/async-job-manager.d.ts +3 -0
- package/dist/async-job-manager.js +56 -16
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +16 -0
- package/dist/cache-stats.d.ts +1 -0
- package/dist/cache-stats.js +19 -11
- package/dist/cli-updater.js +5 -2
- package/dist/codex-json-parser.d.ts +3 -0
- package/dist/codex-json-parser.js +17 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +140 -0
- package/dist/flight-recorder.d.ts +7 -1
- package/dist/flight-recorder.js +33 -6
- package/dist/http-transport.js +21 -18
- package/dist/index.js +104 -34
- package/dist/job-store.d.ts +4 -0
- package/dist/job-store.js +16 -4
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.js +90 -8
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +67 -2
- package/dist/provider-tool-capabilities.d.ts +38 -0
- package/dist/provider-tool-capabilities.js +142 -0
- package/dist/request-context.d.ts +4 -0
- package/dist/request-context.js +16 -0
- package/dist/request-helpers.d.ts +4 -4
- package/dist/request-limits.d.ts +8 -0
- package/dist/request-limits.js +49 -0
- package/dist/secret-redaction.d.ts +3 -0
- package/dist/secret-redaction.js +53 -0
- package/dist/session-manager-pg.js +8 -5
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +2 -0
- package/dist/upstream-contracts.d.ts +27 -0
- package/dist/upstream-contracts.js +131 -0
- package/migrations/004_session_owner_principal.sql +10 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
import { z } from "zod/v3";
|
|
10
10
|
import { executeCli, killAllProcessGroups, providerCommandName } from "./executor.js";
|
|
11
11
|
import { parseStreamJson } from "./stream-json-parser.js";
|
|
12
|
-
import { parseCodexJsonStream } from "./codex-json-parser.js";
|
|
12
|
+
import { parseCodexJsonStream, codexDisplayText, codexFrResponse } from "./codex-json-parser.js";
|
|
13
13
|
import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js";
|
|
14
14
|
import { parseVibeMetaJson } from "./mistral-meta-json-parser.js";
|
|
15
15
|
import { homedir } from "os";
|
|
@@ -25,7 +25,7 @@ import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelA
|
|
|
25
25
|
import { getProviderToolCapabilities } from "./provider-tool-capabilities.js";
|
|
26
26
|
import { AsyncJobManager, } from "./async-job-manager.js";
|
|
27
27
|
import { createJobStore } from "./job-store.js";
|
|
28
|
-
import { ApprovalManager } from "./approval-manager.js";
|
|
28
|
+
import { ApprovalManager, bypassAllowedByOperator, } from "./approval-manager.js";
|
|
29
29
|
import { checkReviewIntegrity } from "./review-integrity.js";
|
|
30
30
|
import { buildClaudeMcpConfig, CLAUDE_MCP_SERVER_NAMES, } from "./claude-mcp-config.js";
|
|
31
31
|
import { resolveGrokSessionArgs, resolveMistralSessionArgs, resolveCodexSessionArgs, sanitizeCliArgValues, prepareMistralRequest as buildMistralCliInvocation, MISTRAL_AGENT_MODES, GATEWAY_SESSION_PREFIX, resolveClaudePermissionFlags, resolveCodexSandboxFlags, CLAUDE_PERMISSION_MODES, GEMINI_APPROVAL_MODES, CODEX_SANDBOX_MODES, CODEX_ASK_FOR_APPROVAL_MODES, CLAUDE_EFFORT_LEVELS, prepareClaudeHighImpactFlags, validateClaudeAgentsMap, prepareCodexHighImpactFlags, prepareCodexForkRequest, CODEX_CONFIG_OVERRIDES_SCHEMA, resolveGeminiSessionPlan, GEMINI_HIGH_IMPACT_PARAMS_SCHEMA, } from "./request-helpers.js";
|
|
@@ -34,7 +34,7 @@ import { resolvePromptInput, PromptPartsSchema, assembleClaudeCacheBlocks, } fro
|
|
|
34
34
|
import { computeSessionCacheStats, computeTtlRemaining, readPersistedRequest, PERSISTED_REQUEST_DEFAULT_MAX_CHARS, } from "./cache-stats.js";
|
|
35
35
|
import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
|
|
36
36
|
import { startHttpGateway } from "./http-transport.js";
|
|
37
|
-
import { getRequestContext } from "./request-context.js";
|
|
37
|
+
import { getRequestContext, resolveOwnerPrincipal, principalCanAccess } from "./request-context.js";
|
|
38
38
|
import { printDoctorJson } from "./doctor.js";
|
|
39
39
|
import { createWorkspace, describeWorkspace, getWorkspace, loadWorkspaceRegistry, registerExistingWorkspace, resolveWorkspaceForProvider, validatePathInsideWorkspace, } from "./workspace-registry.js";
|
|
40
40
|
import { generateSecret, hashSecret } from "./oauth.js";
|
|
@@ -489,13 +489,10 @@ async function resolveWorkspaceAndWorktreeForRequest(args) {
|
|
|
489
489
|
else if (isGatewayAppDirCwd()) {
|
|
490
490
|
throw new Error("No workspace selected. Configure [workspaces].default or pass a registered workspace alias.");
|
|
491
491
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
(args.workingDir || (args.addDir?.length ?? 0) > 0) &&
|
|
497
|
-
!args.runtime.workspaces.allowUnregisteredWorkingDir) {
|
|
498
|
-
throw new Error("workingDir/addDir require a registered workspace alias unless [workspaces].allow_unregistered_working_dir is explicitly enabled.");
|
|
492
|
+
const requestContext = getRequestContext();
|
|
493
|
+
const isRemoteTransport = requestContext?.transport === "http" || requestContext?.authKind === "oauth";
|
|
494
|
+
if (!workspace && isRemoteTransport) {
|
|
495
|
+
throw new Error("Remote HTTP provider requests require a registered workspace alias, session workspace, or [workspaces].default.");
|
|
499
496
|
}
|
|
500
497
|
if (workspace) {
|
|
501
498
|
if (args.workingDir) {
|
|
@@ -720,7 +717,7 @@ export function extractUsageAndCost(cli, output, outputFormat, ctx) {
|
|
|
720
717
|
costUsd: parsed.costUsd ?? undefined,
|
|
721
718
|
};
|
|
722
719
|
}
|
|
723
|
-
if (cli === "codex"
|
|
720
|
+
if (cli === "codex") {
|
|
724
721
|
const parsed = parseCodexJsonStream(output);
|
|
725
722
|
if (!parsed.usage) {
|
|
726
723
|
return {};
|
|
@@ -1277,7 +1274,8 @@ export function prepareClaudeRequest(params, runtime = resolveGatewayServerRunti
|
|
|
1277
1274
|
args.push("--disallowed-tools", ...params.disallowedTools);
|
|
1278
1275
|
}
|
|
1279
1276
|
if (params.approvalStrategy === "mcp_managed") {
|
|
1280
|
-
|
|
1277
|
+
const managedMode = bypassAllowedByOperator() ? "bypassPermissions" : "acceptEdits";
|
|
1278
|
+
args.push("--permission-mode", managedMode);
|
|
1281
1279
|
}
|
|
1282
1280
|
else {
|
|
1283
1281
|
const permFlags = resolveClaudePermissionFlags({
|
|
@@ -1420,9 +1418,7 @@ export function prepareCodexRequest(params, runtime = resolveGatewayServerRuntim
|
|
|
1420
1418
|
if (params.dangerouslyBypassApprovalsAndSandbox) {
|
|
1421
1419
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
1422
1420
|
}
|
|
1423
|
-
|
|
1424
|
-
args.push("--json");
|
|
1425
|
-
}
|
|
1421
|
+
args.push("--json");
|
|
1426
1422
|
args.push("--skip-git-repo-check");
|
|
1427
1423
|
let highImpactCleanup;
|
|
1428
1424
|
if (sessionPlan.mode === "new") {
|
|
@@ -1544,7 +1540,11 @@ export function prepareGeminiRequest(params, runtime = resolveGatewayServerRunti
|
|
|
1544
1540
|
return createApprovalDeniedResponse(params.operation, approvalDecision);
|
|
1545
1541
|
}
|
|
1546
1542
|
}
|
|
1547
|
-
const effectiveApprovalMode = params.approvalStrategy === "mcp_managed"
|
|
1543
|
+
const effectiveApprovalMode = params.approvalStrategy === "mcp_managed"
|
|
1544
|
+
? bypassAllowedByOperator()
|
|
1545
|
+
? "yolo"
|
|
1546
|
+
: "default"
|
|
1547
|
+
: params.approvalMode;
|
|
1548
1548
|
const unsupported = (field, detail) => createErrorResponse(params.operation, 1, "", corrId, new Error(`${field} is not supported by Antigravity CLI (agy): ${detail}`));
|
|
1549
1549
|
if (effectiveApprovalMode &&
|
|
1550
1550
|
effectiveApprovalMode !== "default" &&
|
|
@@ -1651,14 +1651,22 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
|
|
|
1651
1651
|
return createApprovalDeniedResponse(params.operation, approvalDecision);
|
|
1652
1652
|
}
|
|
1653
1653
|
}
|
|
1654
|
-
const
|
|
1654
|
+
const managedGrokBypass = params.approvalStrategy === "mcp_managed" && bypassAllowedByOperator();
|
|
1655
1655
|
const grokContract = UPSTREAM_CLI_CONTRACTS.grok;
|
|
1656
1656
|
const genParams = params;
|
|
1657
1657
|
const args = ["-p", effectivePrompt];
|
|
1658
1658
|
if (resolvedModel)
|
|
1659
1659
|
args.push("--model", resolvedModel);
|
|
1660
1660
|
args.push(...buildArgvFromGeneration(grokContract, GROK_GEN_OUTPUT_FORMAT, genParams));
|
|
1661
|
-
if (
|
|
1661
|
+
if (params.approvalStrategy === "mcp_managed") {
|
|
1662
|
+
if (managedGrokBypass) {
|
|
1663
|
+
args.push("--always-approve");
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
args.push("--permission-mode", "acceptEdits");
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
else if (Boolean(params.alwaysApprove)) {
|
|
1662
1670
|
args.push("--always-approve");
|
|
1663
1671
|
}
|
|
1664
1672
|
else if (params.permissionMode) {
|
|
@@ -1765,7 +1773,9 @@ export function prepareMistralRequest(params, runtime = resolveGatewayServerRunt
|
|
|
1765
1773
|
}
|
|
1766
1774
|
}
|
|
1767
1775
|
const effectivePermissionMode = params.approvalStrategy === "mcp_managed"
|
|
1768
|
-
?
|
|
1776
|
+
? bypassAllowedByOperator()
|
|
1777
|
+
? "auto-approve"
|
|
1778
|
+
: "accept-edits"
|
|
1769
1779
|
: (params.permissionMode ?? "auto-approve");
|
|
1770
1780
|
const prep = buildMistralCliInvocation({
|
|
1771
1781
|
prompt: effectivePrompt,
|
|
@@ -1816,7 +1826,9 @@ export function buildMistralRetryPrep(params, recoveryModel) {
|
|
|
1816
1826
|
resolvedModel: recoveryModel,
|
|
1817
1827
|
outputFormat: params.outputFormat,
|
|
1818
1828
|
permissionMode: params.approvalStrategy === "mcp_managed"
|
|
1819
|
-
?
|
|
1829
|
+
? bypassAllowedByOperator()
|
|
1830
|
+
? "auto-approve"
|
|
1831
|
+
: "accept-edits"
|
|
1820
1832
|
: (params.permissionMode ?? "auto-approve"),
|
|
1821
1833
|
allowedTools: params.allowedTools,
|
|
1822
1834
|
disallowedTools: params.disallowedTools,
|
|
@@ -1830,6 +1842,9 @@ export function buildMistralRetryPrep(params, recoveryModel) {
|
|
|
1830
1842
|
}
|
|
1831
1843
|
function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep, durationMs, resumable, outputFormat, warnings) {
|
|
1832
1844
|
let finalStdout = stdout;
|
|
1845
|
+
if (cli === "codex" && outputFormat !== "json") {
|
|
1846
|
+
finalStdout = codexDisplayText(stdout);
|
|
1847
|
+
}
|
|
1833
1848
|
if (optimizeResponse && outputFormat !== "json") {
|
|
1834
1849
|
const optimized = optimizeResponseText(finalStdout);
|
|
1835
1850
|
logOptimizationTokens("response", corrId, finalStdout, optimized);
|
|
@@ -3619,7 +3634,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3619
3634
|
outputFormat: z
|
|
3620
3635
|
.enum(["text", "json"])
|
|
3621
3636
|
.default("text")
|
|
3622
|
-
.describe("Codex output format.
|
|
3637
|
+
.describe("Codex caller-facing output format. Token/cache usage is recorded in the flight recorder regardless. `text` (default) returns the plain reply; `json` returns the raw `--json` JSONL event stream."),
|
|
3623
3638
|
outputSchema: z
|
|
3624
3639
|
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
3625
3640
|
.optional()
|
|
@@ -3779,7 +3794,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3779
3794
|
logger.info(`[${corrId}] codex_request completed successfully in ${durationMs}ms`);
|
|
3780
3795
|
const codexUsage = extractUsageAndCost("codex", stdout, outputFormat);
|
|
3781
3796
|
safeFlightComplete(corrId, {
|
|
3782
|
-
response: stdout,
|
|
3797
|
+
response: codexFrResponse(outputFormat, stdout),
|
|
3783
3798
|
durationMs,
|
|
3784
3799
|
retryCount: 0,
|
|
3785
3800
|
circuitBreakerState: "closed",
|
|
@@ -4622,7 +4637,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4622
4637
|
outputFormat: z
|
|
4623
4638
|
.enum(["text", "json"])
|
|
4624
4639
|
.default("text")
|
|
4625
|
-
.describe("Codex output format. `
|
|
4640
|
+
.describe("Codex caller-facing output format. Token/cache usage is recorded in the flight recorder regardless. `text` (default) returns the plain reply; `json` returns the raw `--json` JSONL event stream."),
|
|
4626
4641
|
outputSchema: z
|
|
4627
4642
|
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
4628
4643
|
.optional()
|
|
@@ -5160,7 +5175,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5160
5175
|
openWorldHint: false,
|
|
5161
5176
|
}, async ({ jobId }) => {
|
|
5162
5177
|
const job = asyncJobManager.getJobSnapshot(jobId);
|
|
5163
|
-
|
|
5178
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5179
|
+
if (!job || !principalCanAccess(asyncJobManager.getJobOwner(jobId), caller)) {
|
|
5164
5180
|
return {
|
|
5165
5181
|
content: [
|
|
5166
5182
|
{
|
|
@@ -5204,7 +5220,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5204
5220
|
openWorldHint: false,
|
|
5205
5221
|
}, async ({ jobId, maxChars }) => {
|
|
5206
5222
|
const result = asyncJobManager.getJobResult(jobId, maxChars);
|
|
5207
|
-
|
|
5223
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5224
|
+
if (!result || !principalCanAccess(asyncJobManager.getJobOwner(jobId), caller)) {
|
|
5208
5225
|
return {
|
|
5209
5226
|
content: [
|
|
5210
5227
|
{
|
|
@@ -5224,6 +5241,11 @@ export function createGatewayServer(deps = {}) {
|
|
|
5224
5241
|
if (outputFormat === "stream-json" && result.stdout) {
|
|
5225
5242
|
parsed = parseStreamJson(result.stdout);
|
|
5226
5243
|
}
|
|
5244
|
+
if (asyncJobManager.getJobCli(jobId) === "codex" &&
|
|
5245
|
+
outputFormat !== "json" &&
|
|
5246
|
+
result.stdout) {
|
|
5247
|
+
result.stdout = codexDisplayText(result.stdout);
|
|
5248
|
+
}
|
|
5227
5249
|
return {
|
|
5228
5250
|
content: [
|
|
5229
5251
|
{
|
|
@@ -5256,6 +5278,23 @@ export function createGatewayServer(deps = {}) {
|
|
|
5256
5278
|
idempotentHint: true,
|
|
5257
5279
|
openWorldHint: false,
|
|
5258
5280
|
}, async ({ jobId }) => {
|
|
5281
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5282
|
+
if (asyncJobManager.getJobSnapshot(jobId) &&
|
|
5283
|
+
!principalCanAccess(asyncJobManager.getJobOwner(jobId), caller)) {
|
|
5284
|
+
return {
|
|
5285
|
+
content: [
|
|
5286
|
+
{
|
|
5287
|
+
type: "text",
|
|
5288
|
+
text: JSON.stringify({
|
|
5289
|
+
success: false,
|
|
5290
|
+
jobId,
|
|
5291
|
+
reason: "Job not found",
|
|
5292
|
+
}, null, 2),
|
|
5293
|
+
},
|
|
5294
|
+
],
|
|
5295
|
+
isError: true,
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5259
5298
|
const cancel = asyncJobManager.cancelJob(jobId);
|
|
5260
5299
|
if (!cancel.canceled) {
|
|
5261
5300
|
return {
|
|
@@ -5312,7 +5351,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5312
5351
|
maxChars,
|
|
5313
5352
|
includePrompt,
|
|
5314
5353
|
});
|
|
5315
|
-
|
|
5354
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5355
|
+
if (!record || !principalCanAccess(record.ownerPrincipal, caller)) {
|
|
5316
5356
|
return {
|
|
5317
5357
|
content: [
|
|
5318
5358
|
{
|
|
@@ -5738,11 +5778,15 @@ export function createGatewayServer(deps = {}) {
|
|
|
5738
5778
|
openWorldHint: false,
|
|
5739
5779
|
}, async ({ cli }) => {
|
|
5740
5780
|
try {
|
|
5741
|
-
const
|
|
5742
|
-
const
|
|
5743
|
-
|
|
5744
|
-
await sessionManager.getActiveSession(provider)
|
|
5745
|
-
|
|
5781
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5782
|
+
const sessions = (await sessionManager.listSessions(cli)).filter(s => principalCanAccess(s.ownerPrincipal, caller));
|
|
5783
|
+
const activeSessions = Object.fromEntries(await Promise.all(SESSION_PROVIDER_VALUES.map(async (provider) => {
|
|
5784
|
+
const active = await sessionManager.getActiveSession(provider);
|
|
5785
|
+
return [
|
|
5786
|
+
provider,
|
|
5787
|
+
active && principalCanAccess(active.ownerPrincipal, caller) ? active : null,
|
|
5788
|
+
];
|
|
5789
|
+
})));
|
|
5746
5790
|
const sessionList = sessions.map(s => ({
|
|
5747
5791
|
id: s.id,
|
|
5748
5792
|
cli: s.cli,
|
|
@@ -5782,6 +5826,24 @@ export function createGatewayServer(deps = {}) {
|
|
|
5782
5826
|
openWorldHint: false,
|
|
5783
5827
|
}, async ({ cli, sessionId }) => {
|
|
5784
5828
|
try {
|
|
5829
|
+
if (sessionId) {
|
|
5830
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5831
|
+
const target = await sessionManager.getSession(sessionId);
|
|
5832
|
+
if (!target || !principalCanAccess(target.ownerPrincipal, caller)) {
|
|
5833
|
+
return {
|
|
5834
|
+
content: [
|
|
5835
|
+
{
|
|
5836
|
+
type: "text",
|
|
5837
|
+
text: JSON.stringify({
|
|
5838
|
+
success: false,
|
|
5839
|
+
error: "Session not found or does not belong to the specified provider",
|
|
5840
|
+
}, null, 2),
|
|
5841
|
+
},
|
|
5842
|
+
],
|
|
5843
|
+
isError: true,
|
|
5844
|
+
};
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5785
5847
|
const success = await sessionManager.setActiveSession(cli, sessionId || null);
|
|
5786
5848
|
if (!success) {
|
|
5787
5849
|
return {
|
|
@@ -5826,7 +5888,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5826
5888
|
}, async ({ sessionId }) => {
|
|
5827
5889
|
try {
|
|
5828
5890
|
const session = await sessionManager.getSession(sessionId);
|
|
5829
|
-
|
|
5891
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5892
|
+
if (!session || !principalCanAccess(session.ownerPrincipal, caller)) {
|
|
5830
5893
|
return {
|
|
5831
5894
|
content: [
|
|
5832
5895
|
{
|
|
@@ -5873,7 +5936,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5873
5936
|
}, async ({ sessionId }) => {
|
|
5874
5937
|
try {
|
|
5875
5938
|
const session = await sessionManager.getSession(sessionId);
|
|
5876
|
-
|
|
5939
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
5940
|
+
if (!session || !principalCanAccess(session.ownerPrincipal, caller)) {
|
|
5877
5941
|
return {
|
|
5878
5942
|
content: [
|
|
5879
5943
|
{
|
|
@@ -5941,7 +6005,13 @@ export function createGatewayServer(deps = {}) {
|
|
|
5941
6005
|
openWorldHint: false,
|
|
5942
6006
|
}, async ({ cli }) => {
|
|
5943
6007
|
try {
|
|
5944
|
-
const
|
|
6008
|
+
const caller = resolveOwnerPrincipal(getRequestContext());
|
|
6009
|
+
const owned = (await sessionManager.listSessions(cli)).filter(s => principalCanAccess(s.ownerPrincipal, caller));
|
|
6010
|
+
let count = 0;
|
|
6011
|
+
for (const s of owned) {
|
|
6012
|
+
if (await sessionManager.deleteSession(s.id))
|
|
6013
|
+
count++;
|
|
6014
|
+
}
|
|
5945
6015
|
logger.info(`Cleared ${count} sessions${cli ? ` for ${cli}` : ""}`);
|
|
5946
6016
|
return {
|
|
5947
6017
|
content: [
|
package/dist/job-store.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface JobRecord {
|
|
|
18
18
|
finishedAt: string | null;
|
|
19
19
|
pid: number | null;
|
|
20
20
|
expiresAt: string;
|
|
21
|
+
ownerPrincipal: string | null;
|
|
21
22
|
}
|
|
22
23
|
export declare function resolveJobStoreDbPath(): string | null;
|
|
23
24
|
export declare function resolveJobRetentionMs(): number;
|
|
@@ -33,6 +34,7 @@ export interface JobStore {
|
|
|
33
34
|
outputFormat?: string;
|
|
34
35
|
startedAt: string;
|
|
35
36
|
pid: number | null;
|
|
37
|
+
ownerPrincipal?: string | null;
|
|
36
38
|
}): void;
|
|
37
39
|
recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
|
|
38
40
|
recordComplete(input: {
|
|
@@ -88,6 +90,7 @@ export declare class SqliteJobStore implements JobStore {
|
|
|
88
90
|
outputFormat?: string;
|
|
89
91
|
startedAt: string;
|
|
90
92
|
pid: number | null;
|
|
93
|
+
ownerPrincipal?: string | null;
|
|
91
94
|
}): void;
|
|
92
95
|
recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
|
|
93
96
|
recordComplete(input: {
|
|
@@ -127,6 +130,7 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
127
130
|
outputFormat?: string;
|
|
128
131
|
startedAt: string;
|
|
129
132
|
pid: number | null;
|
|
133
|
+
ownerPrincipal?: string | null;
|
|
130
134
|
}): void;
|
|
131
135
|
recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
|
|
132
136
|
recordComplete(input: {
|
package/dist/job-store.js
CHANGED
|
@@ -57,8 +57,16 @@ function rowToRecord(row) {
|
|
|
57
57
|
finishedAt: row.finished_at,
|
|
58
58
|
pid: row.pid,
|
|
59
59
|
expiresAt: row.expires_at,
|
|
60
|
+
ownerPrincipal: row.owner_principal ?? null,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
function ensureJobsOwnerColumn(db) {
|
|
64
|
+
const cols = db.prepare("PRAGMA table_info(jobs)").all();
|
|
65
|
+
const hasOwner = cols.some(col => col?.name === "owner_principal");
|
|
66
|
+
if (!hasOwner) {
|
|
67
|
+
db.exec("ALTER TABLE jobs ADD COLUMN owner_principal TEXT");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
62
70
|
export class SqliteJobStore {
|
|
63
71
|
logger;
|
|
64
72
|
db;
|
|
@@ -94,13 +102,15 @@ export class SqliteJobStore {
|
|
|
94
102
|
started_at TEXT NOT NULL,
|
|
95
103
|
finished_at TEXT,
|
|
96
104
|
pid INTEGER,
|
|
97
|
-
expires_at TEXT NOT NULL
|
|
105
|
+
expires_at TEXT NOT NULL,
|
|
106
|
+
owner_principal TEXT
|
|
98
107
|
);
|
|
99
108
|
CREATE INDEX IF NOT EXISTS idx_jobs_request_key ON jobs(request_key);
|
|
100
109
|
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
101
110
|
CREATE INDEX IF NOT EXISTS idx_jobs_expires_at ON jobs(expires_at);
|
|
102
111
|
CREATE INDEX IF NOT EXISTS idx_jobs_request_key_finished ON jobs(request_key, finished_at);
|
|
103
112
|
`);
|
|
113
|
+
ensureJobsOwnerColumn(this.db);
|
|
104
114
|
if (process.platform !== "win32") {
|
|
105
115
|
try {
|
|
106
116
|
chmodSync(dbPath, 0o600);
|
|
@@ -113,10 +123,10 @@ export class SqliteJobStore {
|
|
|
113
123
|
this.insertStmt = this.db.prepare(`
|
|
114
124
|
INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
|
|
115
125
|
status, exit_code, stdout, stderr, output_truncated, error,
|
|
116
|
-
started_at, finished_at, pid, expires_at)
|
|
126
|
+
started_at, finished_at, pid, expires_at, owner_principal)
|
|
117
127
|
VALUES (@id, @correlation_id, @request_key, @cli, @args_json, @output_format,
|
|
118
128
|
@status, @exit_code, @stdout, @stderr, @output_truncated, @error,
|
|
119
|
-
@started_at, @finished_at, @pid, @expires_at)
|
|
129
|
+
@started_at, @finished_at, @pid, @expires_at, @owner_principal)
|
|
120
130
|
`);
|
|
121
131
|
this.updateOutputStmt = this.db.prepare(`
|
|
122
132
|
UPDATE jobs SET stdout = @stdout, stderr = @stderr, output_truncated = @output_truncated
|
|
@@ -163,12 +173,13 @@ export class SqliteJobStore {
|
|
|
163
173
|
exit_code: null,
|
|
164
174
|
stdout: "",
|
|
165
175
|
stderr: "",
|
|
166
|
-
output_truncated: 0,
|
|
167
176
|
error: null,
|
|
177
|
+
output_truncated: 0,
|
|
168
178
|
started_at: input.startedAt,
|
|
169
179
|
finished_at: null,
|
|
170
180
|
pid: input.pid,
|
|
171
181
|
expires_at: FAR_FUTURE_ISO,
|
|
182
|
+
owner_principal: input.ownerPrincipal ?? null,
|
|
172
183
|
});
|
|
173
184
|
}
|
|
174
185
|
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
@@ -258,6 +269,7 @@ export class MemoryJobStore {
|
|
|
258
269
|
finishedAt: null,
|
|
259
270
|
pid: input.pid,
|
|
260
271
|
expiresAt: FAR_FUTURE_ISO,
|
|
272
|
+
ownerPrincipal: input.ownerPrincipal ?? null,
|
|
261
273
|
});
|
|
262
274
|
}
|
|
263
275
|
recordOutput(id, stdout, stderr, outputTruncated) {
|
package/dist/oauth.d.ts
CHANGED
package/dist/oauth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { URLSearchParams } from "node:url";
|
|
3
|
+
import { readCappedRawBody, maxOAuthBodyBytes } from "./request-limits.js";
|
|
3
4
|
import { issueOAuthAccessToken, timingSafeStringEqual, } from "./auth.js";
|
|
4
5
|
export const OAUTH_CODE_TTL_MS = 5 * 60 * 1000;
|
|
5
6
|
const GENERATED_SECRET_BYTES = 32;
|
|
@@ -52,6 +53,30 @@ export function redactSecret(value) {
|
|
|
52
53
|
function firstHeader(value) {
|
|
53
54
|
return Array.isArray(value) ? value[0] : value;
|
|
54
55
|
}
|
|
56
|
+
const HTML_ESCAPES = {
|
|
57
|
+
"&": "&",
|
|
58
|
+
"<": "<",
|
|
59
|
+
">": ">",
|
|
60
|
+
'"': """,
|
|
61
|
+
"'": "'",
|
|
62
|
+
};
|
|
63
|
+
function escapeHtml(value) {
|
|
64
|
+
return value.replace(/[&<>"']/g, char => HTML_ESCAPES[char] ?? char);
|
|
65
|
+
}
|
|
66
|
+
function readCookie(req, name) {
|
|
67
|
+
const header = firstHeader(req.headers.cookie);
|
|
68
|
+
if (!header)
|
|
69
|
+
return null;
|
|
70
|
+
for (const part of header.split(";")) {
|
|
71
|
+
const idx = part.indexOf("=");
|
|
72
|
+
if (idx < 0)
|
|
73
|
+
continue;
|
|
74
|
+
if (part.slice(0, idx).trim() === name) {
|
|
75
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
55
80
|
function methodNotAllowed(res) {
|
|
56
81
|
res.writeHead(405, { allow: "GET, POST", "content-type": "application/json" });
|
|
57
82
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
@@ -105,12 +130,7 @@ function extractStringArray(value, params, key) {
|
|
|
105
130
|
return values.filter((item) => typeof item === "string" && item.length > 0);
|
|
106
131
|
}
|
|
107
132
|
async function readRawBody(req) {
|
|
108
|
-
return
|
|
109
|
-
const chunks = [];
|
|
110
|
-
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
111
|
-
req.on("error", reject);
|
|
112
|
-
req.on("end", () => resolve(chunks.length ? Buffer.concat(chunks).toString("utf8") : ""));
|
|
113
|
-
});
|
|
133
|
+
return readCappedRawBody(req, maxOAuthBodyBytes());
|
|
114
134
|
}
|
|
115
135
|
async function readOAuthBody(req) {
|
|
116
136
|
const raw = await readRawBody(req);
|
|
@@ -272,8 +292,7 @@ export class OAuthServer {
|
|
|
272
292
|
registrationAllowedByPolicy(req, params) {
|
|
273
293
|
const policy = this.opts.config.registrationPolicy;
|
|
274
294
|
if (policy === "open_dev") {
|
|
275
|
-
|
|
276
|
-
return isLocalHost(host) || process.env.LLM_GATEWAY_OAUTH_OPEN_DEV === "1";
|
|
295
|
+
return process.env.LLM_GATEWAY_OAUTH_OPEN_DEV === "1";
|
|
277
296
|
}
|
|
278
297
|
if (policy === "static_clients")
|
|
279
298
|
return false;
|
|
@@ -372,6 +391,17 @@ export class OAuthServer {
|
|
|
372
391
|
res.end();
|
|
373
392
|
return;
|
|
374
393
|
}
|
|
394
|
+
if (this.opts.config.requireConsent) {
|
|
395
|
+
const isSubmission = req.method === "POST" && params.get("gw_consent") === "1";
|
|
396
|
+
if (!isSubmission) {
|
|
397
|
+
this.renderConsentPage(res, params, clientId, requestedScopes);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!this.verifyConsent(req, params)) {
|
|
401
|
+
this.renderConsentPage(res, params, clientId, requestedScopes, "Incorrect access code.");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
375
405
|
this.pruneExpiredCodes();
|
|
376
406
|
const code = randomUUID();
|
|
377
407
|
this.codes.set(code, {
|
|
@@ -389,6 +419,58 @@ export class OAuthServer {
|
|
|
389
419
|
res.writeHead(302, { location: target.toString() });
|
|
390
420
|
res.end();
|
|
391
421
|
}
|
|
422
|
+
verifyConsent(req, params) {
|
|
423
|
+
const cookie = readCookie(req, "gw_oauth_csrf");
|
|
424
|
+
const formCsrf = params.get("gw_csrf");
|
|
425
|
+
if (!cookie || !formCsrf || !timingSafeStringEqual(cookie, formCsrf))
|
|
426
|
+
return false;
|
|
427
|
+
const secret = params.get("consent_secret") ?? "";
|
|
428
|
+
const hash = this.opts.config.consentSecretHash;
|
|
429
|
+
return Boolean(hash && secret && verifySecret(secret, hash));
|
|
430
|
+
}
|
|
431
|
+
renderConsentPage(res, params, clientId, requestedScopes, error) {
|
|
432
|
+
const csrf = randomUUID();
|
|
433
|
+
const carried = [
|
|
434
|
+
["response_type", params.get("response_type")],
|
|
435
|
+
["client_id", clientId],
|
|
436
|
+
["redirect_uri", params.get("redirect_uri")],
|
|
437
|
+
["scope", params.get("scope")],
|
|
438
|
+
["state", params.get("state")],
|
|
439
|
+
["code_challenge", params.get("code_challenge")],
|
|
440
|
+
["code_challenge_method", params.get("code_challenge_method")],
|
|
441
|
+
["gw_consent", "1"],
|
|
442
|
+
["gw_csrf", csrf],
|
|
443
|
+
];
|
|
444
|
+
const hidden = carried
|
|
445
|
+
.filter(([, value]) => value != null && value !== "")
|
|
446
|
+
.map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(String(value))}">`)
|
|
447
|
+
.join("\n ");
|
|
448
|
+
const scopeList = requestedScopes.map(escapeHtml).join(", ");
|
|
449
|
+
const errorBlock = error ? `<p class="err">${escapeHtml(error)}</p>` : "";
|
|
450
|
+
const html = `<!doctype html>
|
|
451
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
452
|
+
<title>Authorize access</title>
|
|
453
|
+
<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}
|
|
454
|
+
.box{border:1px solid #ddd;border-radius:8px;padding:1.5rem}label{display:block;margin:.75rem 0 .25rem}
|
|
455
|
+
input[type=password]{width:100%;padding:.5rem;box-sizing:border-box}button{margin-top:1rem;padding:.6rem 1.2rem}
|
|
456
|
+
.err{color:#b00020}.muted{color:#555;font-size:.9rem}</style></head>
|
|
457
|
+
<body><div class="box"><h2>Authorize access</h2>
|
|
458
|
+
<p>Application <strong>${escapeHtml(clientId)}</strong> is requesting access to: <strong>${scopeList}</strong>.</p>
|
|
459
|
+
${errorBlock}
|
|
460
|
+
<form method="post" autocomplete="off">
|
|
461
|
+
${hidden}
|
|
462
|
+
<label for="consent_secret">Gateway access code</label>
|
|
463
|
+
<input id="consent_secret" type="password" name="consent_secret" required autofocus>
|
|
464
|
+
<button type="submit">Approve</button>
|
|
465
|
+
</form>
|
|
466
|
+
<p class="muted">Only approve if you initiated this connection.</p></div></body></html>`;
|
|
467
|
+
res.writeHead(200, {
|
|
468
|
+
"content-type": "text/html; charset=utf-8",
|
|
469
|
+
"set-cookie": `gw_oauth_csrf=${csrf}; HttpOnly; SameSite=Lax; Path=/oauth`,
|
|
470
|
+
"cache-control": "no-store",
|
|
471
|
+
});
|
|
472
|
+
res.end(html);
|
|
473
|
+
}
|
|
392
474
|
async handleToken(req, res) {
|
|
393
475
|
if (req.method !== "POST") {
|
|
394
476
|
methodNotAllowed(res);
|
package/dist/pricing.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export interface PricePerMillion {
|
|
|
3
3
|
outputUsd: number;
|
|
4
4
|
cacheReadMultiplier: number;
|
|
5
5
|
}
|
|
6
|
-
export declare const PRICING_AS_OF = "2026-
|
|
6
|
+
export declare const PRICING_AS_OF = "2026-06-13";
|
|
7
7
|
export declare function getPricing(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", model: string): PricePerMillion;
|
|
8
8
|
export declare function estimateCacheSavingsUsd(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", model: string, cacheReadTokens: number): number;
|
package/dist/pricing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PRICING_AS_OF = "2026-
|
|
1
|
+
export const PRICING_AS_OF = "2026-06-13";
|
|
2
2
|
const ANTHROPIC_SONNET = {
|
|
3
3
|
inputUsd: 3,
|
|
4
4
|
outputUsd: 15,
|
|
@@ -19,6 +19,41 @@ const OPENAI_GPT5 = {
|
|
|
19
19
|
outputUsd: 10,
|
|
20
20
|
cacheReadMultiplier: 0.5,
|
|
21
21
|
};
|
|
22
|
+
const GEMINI_25_PRO = {
|
|
23
|
+
inputUsd: 1.25,
|
|
24
|
+
outputUsd: 10,
|
|
25
|
+
cacheReadMultiplier: 0.1,
|
|
26
|
+
};
|
|
27
|
+
const GEMINI_FLASH = {
|
|
28
|
+
inputUsd: 0.3,
|
|
29
|
+
outputUsd: 2.5,
|
|
30
|
+
cacheReadMultiplier: 0.1,
|
|
31
|
+
};
|
|
32
|
+
const GEMINI_3_PRO = {
|
|
33
|
+
inputUsd: 2,
|
|
34
|
+
outputUsd: 12,
|
|
35
|
+
cacheReadMultiplier: 0.1,
|
|
36
|
+
};
|
|
37
|
+
const GROK_4 = {
|
|
38
|
+
inputUsd: 1.25,
|
|
39
|
+
outputUsd: 2.5,
|
|
40
|
+
cacheReadMultiplier: 0.16,
|
|
41
|
+
};
|
|
42
|
+
const GROK_BUILD = {
|
|
43
|
+
inputUsd: 1,
|
|
44
|
+
outputUsd: 2,
|
|
45
|
+
cacheReadMultiplier: 0.2,
|
|
46
|
+
};
|
|
47
|
+
const MISTRAL_MEDIUM = {
|
|
48
|
+
inputUsd: 1.5,
|
|
49
|
+
outputUsd: 7.5,
|
|
50
|
+
cacheReadMultiplier: 0.1,
|
|
51
|
+
};
|
|
52
|
+
const MISTRAL_DEVSTRAL = {
|
|
53
|
+
inputUsd: 0.1,
|
|
54
|
+
outputUsd: 0.3,
|
|
55
|
+
cacheReadMultiplier: 0.1,
|
|
56
|
+
};
|
|
22
57
|
const ZERO = {
|
|
23
58
|
inputUsd: 0,
|
|
24
59
|
outputUsd: 0,
|
|
@@ -33,13 +68,43 @@ export function getPricing(cli, model) {
|
|
|
33
68
|
return ANTHROPIC_OPUS;
|
|
34
69
|
if (lower.includes("haiku"))
|
|
35
70
|
return ANTHROPIC_HAIKU;
|
|
36
|
-
return
|
|
71
|
+
return ANTHROPIC_SONNET;
|
|
37
72
|
}
|
|
38
73
|
if (cli === "codex") {
|
|
39
74
|
if (lower.includes("gpt-5") || lower.includes("o3"))
|
|
40
75
|
return OPENAI_GPT5;
|
|
41
76
|
return ZERO;
|
|
42
77
|
}
|
|
78
|
+
if (cli === "gemini") {
|
|
79
|
+
const specialtyVariant = lower.includes("lite") ||
|
|
80
|
+
lower.includes("image") ||
|
|
81
|
+
lower.includes("audio") ||
|
|
82
|
+
lower.includes("tts");
|
|
83
|
+
if (!specialtyVariant) {
|
|
84
|
+
if (lower.includes("2.5-flash"))
|
|
85
|
+
return GEMINI_FLASH;
|
|
86
|
+
if (lower.includes("2.5-pro"))
|
|
87
|
+
return GEMINI_25_PRO;
|
|
88
|
+
if (lower.includes("gemini-3") && lower.includes("pro"))
|
|
89
|
+
return GEMINI_3_PRO;
|
|
90
|
+
}
|
|
91
|
+
return ZERO;
|
|
92
|
+
}
|
|
93
|
+
if (cli === "grok") {
|
|
94
|
+
if (lower.includes("grok-build") || lower.includes("grok-code"))
|
|
95
|
+
return GROK_BUILD;
|
|
96
|
+
if (lower.includes("grok-4") || lower.includes("grok-3") || lower.includes("grok-latest")) {
|
|
97
|
+
return GROK_4;
|
|
98
|
+
}
|
|
99
|
+
return ZERO;
|
|
100
|
+
}
|
|
101
|
+
if (cli === "mistral") {
|
|
102
|
+
if (lower.includes("devstral-small"))
|
|
103
|
+
return MISTRAL_DEVSTRAL;
|
|
104
|
+
if (lower.includes("mistral-medium-3.5"))
|
|
105
|
+
return MISTRAL_MEDIUM;
|
|
106
|
+
return ZERO;
|
|
107
|
+
}
|
|
43
108
|
return ZERO;
|
|
44
109
|
}
|
|
45
110
|
export function estimateCacheSavingsUsd(cli, model, cacheReadTokens) {
|