@yansigit/opencodex 2.33.0 → 2.35.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/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -97,6 +97,9 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
97
97
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
98
98
|
import type { ManagementContext } from "./context";
|
|
99
99
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
100
|
+
import { resolveAiStudioCredentials } from "../../oauth/aistudio-credentials";
|
|
101
|
+
import { buildAiStudioHeaders, parseGoogleCookieJar } from "../../oauth/google-aistudio-auth";
|
|
102
|
+
import { maxWsFrameBytesConfigError, wsUpstreamConfigError } from "../../config/provider-validation";
|
|
100
103
|
|
|
101
104
|
type ProviderPatchApplication =
|
|
102
105
|
| { error: string }
|
|
@@ -108,6 +111,93 @@ type ProviderPatchApplication =
|
|
|
108
111
|
headersTouched: boolean;
|
|
109
112
|
};
|
|
110
113
|
|
|
114
|
+
const AI_STUDIO_REAUTH_ERROR = "Session expired or missing — re-authentication required";
|
|
115
|
+
const AI_STUDIO_PROBE_TIMEOUT_MS = 8_000;
|
|
116
|
+
const AI_STUDIO_PROBE_MODEL = "gemini-2.5-flash";
|
|
117
|
+
const AI_STUDIO_ORIGIN = "https://aistudio.google.com";
|
|
118
|
+
|
|
119
|
+
let aiStudioProbeFetchForTests: typeof fetch | undefined;
|
|
120
|
+
|
|
121
|
+
export function setAiStudioProbeFetchForTests(fetchImpl?: typeof fetch): void {
|
|
122
|
+
aiStudioProbeFetchForTests = fetchImpl;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isAiStudioHtmlSignIn(text: string): boolean {
|
|
126
|
+
const lower = text.trim().toLowerCase();
|
|
127
|
+
return lower.startsWith("<!doctype") || lower.startsWith("<html") || lower.includes("accounts.google.com/v3/signin");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function probeAiStudioLiveSession(
|
|
131
|
+
name: string,
|
|
132
|
+
prov: OcxProviderConfig,
|
|
133
|
+
): Promise<{ ok: boolean; latencyMs: number; authState?: "connected" | "checking" | "needs_reauth" | "unsupported"; message?: string; error?: string }> {
|
|
134
|
+
const credentials = resolveAiStudioCredentials(prov);
|
|
135
|
+
if (credentials.kind !== "ready") {
|
|
136
|
+
return { ok: false, latencyMs: 0, error: AI_STUDIO_REAUTH_ERROR };
|
|
137
|
+
}
|
|
138
|
+
if (process.platform !== "darwin") {
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
latencyMs: 0,
|
|
142
|
+
authState: "unsupported",
|
|
143
|
+
message: "AI Studio credentials configured; native login is unsupported on this platform",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const base = (prov.baseUrl || "https://alkalimakersuite-pa.clients6.google.com").replace(/\/+$/, "");
|
|
148
|
+
const url = base + "/v1internal:generateContent";
|
|
149
|
+
const jar = parseGoogleCookieJar(credentials.cookieHeader);
|
|
150
|
+
const headers = await buildAiStudioHeaders(jar, AI_STUDIO_ORIGIN);
|
|
151
|
+
const body = JSON.stringify({
|
|
152
|
+
model: AI_STUDIO_PROBE_MODEL,
|
|
153
|
+
contents: [{ role: "user", parts: [{ text: "ping" }] }],
|
|
154
|
+
generationConfig: { maxOutputTokens: 1 },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timer = setTimeout(() => controller.abort(), AI_STUDIO_PROBE_TIMEOUT_MS);
|
|
159
|
+
const started = Date.now();
|
|
160
|
+
try {
|
|
161
|
+
const outboundProvider = aiStudioProbeFetchForTests
|
|
162
|
+
? { ...prov, fetch: aiStudioProbeFetchForTests }
|
|
163
|
+
: prov;
|
|
164
|
+
const response = await providerOutboundPost(name, outboundProvider, url, {
|
|
165
|
+
headers,
|
|
166
|
+
body,
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
});
|
|
169
|
+
const latencyMs = Date.now() - started;
|
|
170
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
171
|
+
const text = await response.text().catch(() => "");
|
|
172
|
+
|
|
173
|
+
if ((response.status >= 300 && response.status < 400) || response.status === 401 || response.status === 403) {
|
|
174
|
+
return { ok: false, latencyMs, error: AI_STUDIO_REAUTH_ERROR };
|
|
175
|
+
}
|
|
176
|
+
if (contentType.includes("text/html") || isAiStudioHtmlSignIn(text)) {
|
|
177
|
+
return { ok: false, latencyMs, error: AI_STUDIO_REAUTH_ERROR };
|
|
178
|
+
}
|
|
179
|
+
if (response.status !== 200) {
|
|
180
|
+
return { ok: false, latencyMs, error: "AI Studio connection probe failed" };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
JSON.parse(text);
|
|
184
|
+
} catch {
|
|
185
|
+
return { ok: false, latencyMs, error: "AI Studio connection probe failed" };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
latencyMs,
|
|
190
|
+
authState: "connected",
|
|
191
|
+
message: "AI Studio session verified",
|
|
192
|
+
};
|
|
193
|
+
} catch {
|
|
194
|
+
return { ok: false, latencyMs: Date.now() - started, error: "AI Studio connection probe failed" };
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
111
201
|
/**
|
|
112
202
|
* Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
|
|
113
203
|
* for validation and again inside the config mutation lock against the newest provider,
|
|
@@ -206,6 +296,28 @@ function applyProviderPatchFields(
|
|
|
206
296
|
next.liveModels = rawBody.liveModels;
|
|
207
297
|
touched = true;
|
|
208
298
|
}
|
|
299
|
+
if (Object.hasOwn(rawBody, "wsUpstream")) {
|
|
300
|
+
const value = rawBody.wsUpstream;
|
|
301
|
+
if (value === null) {
|
|
302
|
+
delete next.wsUpstream;
|
|
303
|
+
} else {
|
|
304
|
+
const error = wsUpstreamConfigError(value);
|
|
305
|
+
if (error) return { error };
|
|
306
|
+
next.wsUpstream = value as boolean;
|
|
307
|
+
}
|
|
308
|
+
touched = true;
|
|
309
|
+
}
|
|
310
|
+
if (Object.hasOwn(rawBody, "maxWsFrameBytes")) {
|
|
311
|
+
const value = rawBody.maxWsFrameBytes;
|
|
312
|
+
if (value === null) {
|
|
313
|
+
delete next.maxWsFrameBytes;
|
|
314
|
+
} else {
|
|
315
|
+
const error = maxWsFrameBytesConfigError(value);
|
|
316
|
+
if (error) return { error };
|
|
317
|
+
next.maxWsFrameBytes = value as number;
|
|
318
|
+
}
|
|
319
|
+
touched = true;
|
|
320
|
+
}
|
|
209
321
|
if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
|
|
210
322
|
if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
|
|
211
323
|
if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
|
|
@@ -482,6 +594,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
482
594
|
contextWindow: p.contextWindow,
|
|
483
595
|
modelContextWindows: p.modelContextWindows,
|
|
484
596
|
modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
|
|
597
|
+
wsUpstream: p.wsUpstream,
|
|
598
|
+
maxWsFrameBytes: p.maxWsFrameBytes,
|
|
485
599
|
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
486
600
|
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
487
601
|
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
@@ -575,15 +689,26 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
575
689
|
let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
|
|
576
690
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
577
691
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
578
|
-
const
|
|
692
|
+
const submittedProvider = isPlainRecord(body.provider)
|
|
693
|
+
? structuredClone(body.provider) as Record<string, unknown>
|
|
694
|
+
: body.provider;
|
|
695
|
+
const submittedCredential = isPlainRecord(submittedProvider)
|
|
696
|
+
&& isPlainRecord(submittedProvider.azureCredential)
|
|
697
|
+
? submittedProvider.azureCredential as Record<string, unknown>
|
|
698
|
+
: undefined;
|
|
699
|
+
if (typeof submittedCredential?.managedIdentityClientId === "string") {
|
|
700
|
+
submittedCredential.managedIdentityClientId = submittedCredential.managedIdentityClientId.trim();
|
|
701
|
+
}
|
|
702
|
+
const providerError = providerManagementConfigError(name, submittedProvider);
|
|
579
703
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
580
|
-
const serviceTierError = providerServiceTierConfigError(name,
|
|
704
|
+
const serviceTierError = providerServiceTierConfigError(name, submittedProvider);
|
|
581
705
|
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
582
|
-
const prov =
|
|
706
|
+
const prov = submittedProvider ? stripCodexRuntimeProviderFields(submittedProvider as OcxProviderConfig) : undefined;
|
|
583
707
|
// PATCH already clears on null; POST persisted the body as submitted, so a `null` here
|
|
584
708
|
// reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
|
|
585
709
|
// "clear" means everywhere else.
|
|
586
710
|
if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
|
|
711
|
+
if (prov && prov.wsUpstream === null) delete prov.wsUpstream;
|
|
587
712
|
if (!name || !prov?.adapter || !prov?.baseUrl) {
|
|
588
713
|
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
|
|
589
714
|
}
|
|
@@ -628,6 +753,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
628
753
|
// erase hand-edited per-model prices from Logs/Usage estimates.
|
|
629
754
|
const existingCosts = config.providers[name]?.modelCosts;
|
|
630
755
|
if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
|
|
756
|
+
// And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no
|
|
757
|
+
// member for it either, so an add/edit save structurally cannot carry it — and dropping it
|
|
758
|
+
// silently ENABLES rotation, because activation is presence-driven once the knob is gone.
|
|
759
|
+
// An overwrite must not spend a second subscription account's quota as a side effect.
|
|
760
|
+
const existingFailover = config.providers[name]?.oauthAccountFailover;
|
|
761
|
+
if (existingFailover && !prov.oauthAccountFailover) prov.oauthAccountFailover = existingFailover;
|
|
631
762
|
// ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
|
|
632
763
|
// has no member for either field, so the add/edit form structurally cannot send them:
|
|
633
764
|
// absence in the request means "not carried", never "the user deleted it". Deletion goes
|
|
@@ -838,6 +969,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
838
969
|
message: "Passthrough provider is configured (forwards your Codex login; no upstream /models).",
|
|
839
970
|
});
|
|
840
971
|
}
|
|
972
|
+
if (prov.googleMode === "ai-studio-web" || name === "google-aistudio") {
|
|
973
|
+
return jsonResponse(await probeAiStudioLiveSession(name, prov));
|
|
974
|
+
}
|
|
841
975
|
if (prov.liveModels === false) {
|
|
842
976
|
// A static catalog has no live discovery endpoint to test. This is neither
|
|
843
977
|
// positive connectivity evidence nor an outage, and it must stay before
|
|
@@ -20,7 +20,7 @@ import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/ass
|
|
|
20
20
|
import { activateLab, labActivationRequired } from "../../lib/lab-activation";
|
|
21
21
|
import { quotaEvidenceForCandidate } from "../../routing/quota";
|
|
22
22
|
import { routedProviderConfig } from "../../router";
|
|
23
|
-
import { saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
|
|
23
|
+
import { deleteConfigTopLevelKey, saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
|
|
24
24
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
25
25
|
import { isPlainRecord } from "./shared";
|
|
26
26
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
@@ -337,7 +337,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
337
337
|
const nextProfiles = { ...(config.routingProfiles ?? {}) };
|
|
338
338
|
delete nextProfiles[id];
|
|
339
339
|
if (Object.keys(nextProfiles).length > 0) config.routingProfiles = nextProfiles;
|
|
340
|
-
else
|
|
340
|
+
else deleteConfigTopLevelKey(config, "routingProfiles");
|
|
341
341
|
const saveConfigPreservingClaudeCodeSafe = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
|
|
342
342
|
saveConfigPreservingClaudeCodeSafe(config);
|
|
343
343
|
reconcileLiveStateStores();
|
|
@@ -70,6 +70,7 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
|
|
|
70
70
|
import { handleComboRoutes } from "./management/combo-routes";
|
|
71
71
|
import { handleSystemRoutes } from "./management/system-routes";
|
|
72
72
|
import { handleSidebarRoutes } from "./management/sidebar-routes";
|
|
73
|
+
import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
|
|
73
74
|
import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
74
75
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
75
76
|
import type { ManagementContext } from "./management/context";
|
|
@@ -229,6 +230,7 @@ export async function handleManagementAPI(
|
|
|
229
230
|
?? (await handleIntegrationRoutes(ctx))
|
|
230
231
|
?? (await handleNativeIntegrationRoutes(ctx))
|
|
231
232
|
?? (await handleAgentSettingsRoutes(ctx))
|
|
233
|
+
?? (await handleCodexPromptRoutes(ctx))
|
|
232
234
|
?? (await handleOauthAccountRoutes(ctx))
|
|
233
235
|
?? (await handleComboRoutes(ctx))
|
|
234
236
|
?? (await handleSystemRoutes(ctx))
|
|
@@ -144,6 +144,24 @@ export function scanListenPids(port: number): ListenPidScan {
|
|
|
144
144
|
.filter(pid => Number.isSafeInteger(pid) && pid > 0),
|
|
145
145
|
};
|
|
146
146
|
} catch (lsofErr) {
|
|
147
|
+
if (lsofErr && typeof lsofErr === "object" && "status" in lsofErr && lsofErr.status === 1) {
|
|
148
|
+
return { ok: true, pids: [] };
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const output = execFileSync("ss", ["-tlnp", `sport = :${port}`], {
|
|
152
|
+
encoding: "utf-8",
|
|
153
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
154
|
+
timeout: 3000,
|
|
155
|
+
});
|
|
156
|
+
const pids = new Set<number>();
|
|
157
|
+
for (const match of output.matchAll(/pid=(\d+)/g)) {
|
|
158
|
+
const pid = Number(match[1]);
|
|
159
|
+
if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, pids: [...pids] };
|
|
162
|
+
} catch {
|
|
163
|
+
/* ss unavailable or unsupported on this platform; try netstat */
|
|
164
|
+
}
|
|
147
165
|
try {
|
|
148
166
|
const output = execFileSync("netstat", ["-anlp"], {
|
|
149
167
|
encoding: "utf-8",
|
|
@@ -154,7 +172,7 @@ export function scanListenPids(port: number): ListenPidScan {
|
|
|
154
172
|
} catch (netstatErr) {
|
|
155
173
|
return {
|
|
156
174
|
ok: false,
|
|
157
|
-
error: `lsof/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
|
|
175
|
+
error: `lsof/ss/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
|
|
158
176
|
};
|
|
159
177
|
}
|
|
160
178
|
}
|
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
* up to the drain window.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
adapterEofIncompleteFrame,
|
|
29
|
+
createSseTerminalOutputBoundary,
|
|
30
|
+
doneFrame,
|
|
31
|
+
failedTailFrame,
|
|
32
|
+
} from "./relay";
|
|
28
33
|
import {
|
|
29
34
|
nextSseBlock,
|
|
30
35
|
payloadRewriteAsBlockRewrite,
|
|
@@ -100,9 +105,19 @@ export function relaySseEagerBounded(
|
|
|
100
105
|
const now = opts?.now ?? Date.now;
|
|
101
106
|
|
|
102
107
|
const reader = body.getReader();
|
|
108
|
+
const terminalEncoder = new TextEncoder();
|
|
109
|
+
const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
|
|
110
|
+
const terminalSentinel = doneFrame(terminalEncoder);
|
|
103
111
|
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
104
112
|
const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
|
|
105
113
|
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
|
|
114
|
+
const encodeFailedTail = (error: unknown): Uint8Array | null => {
|
|
115
|
+
try {
|
|
116
|
+
return failedTailFrame(terminalEncoder, error);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
106
121
|
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
107
122
|
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
108
123
|
const rewriteBudget = opts?.rewriteBudget;
|
|
@@ -164,13 +179,21 @@ export function relaySseEagerBounded(
|
|
|
164
179
|
let queuedBytes = 0;
|
|
165
180
|
let cancelled = false;
|
|
166
181
|
let done = false;
|
|
167
|
-
const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
|
|
168
182
|
// Pause gate: resolved by client pull, client cancel, or upstream abort so a
|
|
169
183
|
// paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
|
|
170
184
|
// turn unregistration stay reachable, drainAndShutdown never hangs).
|
|
171
185
|
let wake: (() => void) | null = null;
|
|
172
186
|
const wakeUp = () => { const w = wake; wake = null; w?.(); };
|
|
173
|
-
const paused = () => new Promise<void>(resolve => {
|
|
187
|
+
const paused = () => new Promise<void>(resolve => {
|
|
188
|
+
wake = resolve;
|
|
189
|
+
// A pull, cancel, or abort can win the tiny window between the loop's
|
|
190
|
+
// predicate check and installing this resolver. Re-check every wake
|
|
191
|
+
// predicate after installation so that an already-fired wake cannot leave
|
|
192
|
+
// the producer parked forever.
|
|
193
|
+
if (queuedBytes <= maxQueueBytes || cancelled || upstream.signal.aborted) {
|
|
194
|
+
wakeUp();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
174
197
|
upstream.signal.addEventListener("abort", wakeUp, { once: true });
|
|
175
198
|
|
|
176
199
|
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
@@ -196,6 +219,9 @@ export function relaySseEagerBounded(
|
|
|
196
219
|
|
|
197
220
|
const producer = async () => {
|
|
198
221
|
let syntheticKind: "incomplete" | "failed" | null = null;
|
|
222
|
+
let deliveryFallbackSent = false;
|
|
223
|
+
let priorRewriteFailure = false;
|
|
224
|
+
let priorRewriteError: unknown;
|
|
199
225
|
// reader.read() is not intrinsically tied to the upstream AbortController
|
|
200
226
|
// (a fetch body usually rejects on abort, but that coupling is the fetch
|
|
201
227
|
// implementation's, not the stream's), so abort must break a parked read on
|
|
@@ -221,18 +247,46 @@ export function relaySseEagerBounded(
|
|
|
221
247
|
if (upstreamDone) {
|
|
222
248
|
hooks.finishInspection();
|
|
223
249
|
const boundedTail = terminalBoundary.finish();
|
|
250
|
+
let clientTail = boundedTail;
|
|
251
|
+
let rewriteFailed = false;
|
|
252
|
+
let rewriteError: unknown;
|
|
224
253
|
if (activeRewrite) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
254
|
+
try {
|
|
255
|
+
const rewritten = rewriteOutbound(boundedTail);
|
|
256
|
+
clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
|
|
257
|
+
} catch (error) {
|
|
258
|
+
rewriteFailed = true;
|
|
259
|
+
rewriteError = error;
|
|
260
|
+
clientTail = new Uint8Array(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (rewriteFailed) {
|
|
264
|
+
const safeTail = encodeFailedTail(rewriteError);
|
|
265
|
+
if (safeTail && !cancelled && !upstream.signal.aborted) {
|
|
266
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
267
|
+
queuedBytes += safeTail.byteLength;
|
|
268
|
+
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
269
|
+
try { controllerRef?.close(); } catch { /* client already gone */ }
|
|
230
270
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (clientTail.byteLength > 0 && !cancelled) {
|
|
274
|
+
queuedBytes += clientTail.byteLength;
|
|
275
|
+
try { controllerRef?.enqueue(clientTail); } catch { /* client already gone */ }
|
|
234
276
|
}
|
|
235
|
-
if (
|
|
277
|
+
if (terminalBoundary.terminalSeen()) {
|
|
278
|
+
if (!terminalBoundary.doneSeen() && !cancelled) {
|
|
279
|
+
queuedBytes += terminalSentinel.byteLength;
|
|
280
|
+
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
281
|
+
}
|
|
282
|
+
} else if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
|
|
283
|
+
// A clean 200 EOF without a Responses terminal must be visible to
|
|
284
|
+
// Codex as one incomplete turn, followed by the normal sentinel.
|
|
285
|
+
queuedBytes += adapterEofFrame.byteLength + terminalSentinel.byteLength;
|
|
286
|
+
try {
|
|
287
|
+
controllerRef?.enqueue(adapterEofFrame);
|
|
288
|
+
controllerRef?.enqueue(terminalSentinel);
|
|
289
|
+
} catch { /* client already gone */ }
|
|
236
290
|
syntheticKind = "incomplete";
|
|
237
291
|
}
|
|
238
292
|
break;
|
|
@@ -247,7 +301,21 @@ export function relaySseEagerBounded(
|
|
|
247
301
|
continue;
|
|
248
302
|
}
|
|
249
303
|
const terminalBounded = terminalBoundary.feed(value);
|
|
250
|
-
|
|
304
|
+
let outbound: Uint8Array;
|
|
305
|
+
if (activeRewrite) {
|
|
306
|
+
try {
|
|
307
|
+
outbound = rewriteOutbound(terminalBounded);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// Preserve the first rewrite failure across the teardown flush. A
|
|
310
|
+
// second empty flush may succeed, but the terminal bytes still
|
|
311
|
+
// must not bypass the failed rewrite or become DONE-only output.
|
|
312
|
+
priorRewriteFailure = true;
|
|
313
|
+
priorRewriteError = error;
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
outbound = terminalBounded;
|
|
318
|
+
}
|
|
251
319
|
if (outbound.byteLength > 0) {
|
|
252
320
|
queuedBytes += outbound.byteLength;
|
|
253
321
|
try {
|
|
@@ -278,16 +346,75 @@ export function relaySseEagerBounded(
|
|
|
278
346
|
} catch (err) {
|
|
279
347
|
// Upstream read failure. Distinguish genuine mid-stream reset from
|
|
280
348
|
// abort-driven teardown (shutdown/cancel-expiry) — audit M3.
|
|
281
|
-
|
|
349
|
+
// A read can fail after delivering an unterminated terminal block. Flush
|
|
350
|
+
// both observers before deciding whether this is a synthetic reset so
|
|
351
|
+
// eager mode matches the tee/pull boundary semantics at EOF.
|
|
352
|
+
let boundedTail: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
|
353
|
+
let tailTerminal = false;
|
|
354
|
+
let tailDone = false;
|
|
355
|
+
try { hooks.finishInspection(); } catch { /* preserve the original read failure */ }
|
|
356
|
+
try {
|
|
357
|
+
boundedTail = terminalBoundary.finish();
|
|
358
|
+
tailTerminal = terminalBoundary.terminalSeen();
|
|
359
|
+
tailDone = terminalBoundary.doneSeen();
|
|
360
|
+
} catch {
|
|
361
|
+
// A near-cap ambiguous delimiter tail may itself overflow at EOF.
|
|
362
|
+
// Preserve the original read/framing failure and continue emitting
|
|
363
|
+
// the bounded failed tail instead of letting cleanup throw again.
|
|
364
|
+
}
|
|
365
|
+
let clientTail: Uint8Array<ArrayBufferLike> = boundedTail;
|
|
366
|
+
let rewriteFailed = false;
|
|
367
|
+
let rewriteError: unknown;
|
|
368
|
+
if (priorRewriteFailure) {
|
|
369
|
+
rewriteFailed = true;
|
|
370
|
+
rewriteError = priorRewriteError;
|
|
371
|
+
clientTail = new Uint8Array(0);
|
|
372
|
+
} else if (activeRewrite) {
|
|
373
|
+
try {
|
|
374
|
+
const rewritten = rewriteOutbound(boundedTail);
|
|
375
|
+
clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
|
|
376
|
+
} catch (error) {
|
|
377
|
+
rewriteFailed = true;
|
|
378
|
+
rewriteError = error;
|
|
379
|
+
clientTail = new Uint8Array(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (clientTail.byteLength > 0 && !cancelled && !upstream.signal.aborted) {
|
|
383
|
+
queuedBytes += clientTail.byteLength;
|
|
384
|
+
try { controllerRef?.enqueue(clientTail); } catch { /* client already torn down */ }
|
|
385
|
+
}
|
|
386
|
+
if (rewriteFailed && !cancelled && !upstream.signal.aborted) {
|
|
387
|
+
// Never bypass a client rewrite after it fails: boundedTail can contain
|
|
388
|
+
// provider metadata or content that the active rewrite was required to
|
|
389
|
+
// remove. Emit one safe failed envelope instead. When inspection has
|
|
390
|
+
// already reported the real upstream terminal, this is a delivery
|
|
391
|
+
// fallback only and must not create a second accounting outcome.
|
|
392
|
+
const safeTail = encodeFailedTail(rewriteError ?? err);
|
|
393
|
+
if (safeTail && !cancelled && !upstream.signal.aborted) {
|
|
394
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
395
|
+
deliveryFallbackSent = true;
|
|
396
|
+
queuedBytes += safeTail.byteLength;
|
|
397
|
+
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
398
|
+
try { controllerRef?.close(); } catch { /* client already gone */ }
|
|
399
|
+
}
|
|
400
|
+
} else if (tailTerminal && !cancelled && !upstream.signal.aborted) {
|
|
401
|
+
if (!tailDone) {
|
|
402
|
+
queuedBytes += terminalSentinel.byteLength;
|
|
403
|
+
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
404
|
+
}
|
|
405
|
+
} else if (!tailTerminal && !cancelled && !upstream.signal.aborted) {
|
|
282
406
|
// Serializing `err` can run user-defined accessors (Error.message
|
|
283
407
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
284
408
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
285
409
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
286
|
-
const tail =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
410
|
+
const tail = encodeFailedTail(err);
|
|
411
|
+
if (tail && !cancelled && !upstream.signal.aborted) {
|
|
412
|
+
// Inspection and client framing have separate bounded parsers. If
|
|
413
|
+
// inspection resynchronized after an oversized frame and observed a
|
|
414
|
+
// later real terminal, it still must not suppress a terminal delivery
|
|
415
|
+
// to the client. Only accounting remains tied to the inspected result.
|
|
416
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
417
|
+
deliveryFallbackSent = true;
|
|
291
418
|
queuedBytes += tail.byteLength;
|
|
292
419
|
try { controllerRef?.enqueue(tail); } catch { /* client already torn down */ }
|
|
293
420
|
try { controllerRef?.close(); } catch { /* client already torn down */ }
|
|
@@ -306,7 +433,7 @@ export function relaySseEagerBounded(
|
|
|
306
433
|
if (cancelled && !hooks.sawTerminal()) {
|
|
307
434
|
hooks.onClientCancel();
|
|
308
435
|
}
|
|
309
|
-
if (cancelled || upstream.signal.aborted || syntheticKind === "failed") {
|
|
436
|
+
if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
|
|
310
437
|
upstream.abort();
|
|
311
438
|
reader.cancel().catch(() => {});
|
|
312
439
|
}
|