@bitkyc08/opencodex 2.55.0 → 2.56.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/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../../service";
|
|
2
|
+
import {
|
|
3
|
+
createWindowsTaskListingCache,
|
|
4
|
+
inspectNativeCodexOwnership,
|
|
5
|
+
type NativeCodexOwnership,
|
|
6
|
+
type OwnershipInspection,
|
|
7
|
+
} from "../../integrations/native/ownership-preflight";
|
|
8
|
+
import { registerCodexQuotaAutoRefreshWorker } from "../../codex/quota-auto-refresh";
|
|
9
|
+
import {
|
|
10
|
+
consumeForInspection,
|
|
11
|
+
relaySseWithHeartbeat,
|
|
12
|
+
relayWithAbort,
|
|
13
|
+
responseWithDeferredRequestLog,
|
|
14
|
+
sanitizePassthroughHeaders,
|
|
15
|
+
} from "../relay";
|
|
16
|
+
import {
|
|
17
|
+
assertServerAuthConfig,
|
|
18
|
+
corsHeaders,
|
|
19
|
+
managementCorsHeaders,
|
|
20
|
+
isAllowedRequestOrigin,
|
|
21
|
+
isAllowedManagementOrigin,
|
|
22
|
+
isApiAuthRequired,
|
|
23
|
+
isLoopbackHostname,
|
|
24
|
+
jsonResponse,
|
|
25
|
+
admissionFields,
|
|
26
|
+
resolveApiAuth,
|
|
27
|
+
resolveResponsesApiAuth,
|
|
28
|
+
requestPolicyView,
|
|
29
|
+
type DataPlaneAdmission,
|
|
30
|
+
type RequestPolicyView,
|
|
31
|
+
safeConfigDTO,
|
|
32
|
+
setCorsOrigin,
|
|
33
|
+
withCors,
|
|
34
|
+
withManagementCors,
|
|
35
|
+
} from "../auth-cors";
|
|
36
|
+
import {
|
|
37
|
+
bindNativeMainStartupLifecycle,
|
|
38
|
+
blockNativeMainStartupForUnownedServiceHome,
|
|
39
|
+
prepareNativeMainStartupLifecycle,
|
|
40
|
+
releaseNativeMainStartupLifecycle,
|
|
41
|
+
type NativeMainStartupGateDeps,
|
|
42
|
+
type NativeMainStartupLifecycle,
|
|
43
|
+
} from "../../codex/native-profile-startup";
|
|
44
|
+
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "../management-api";
|
|
45
|
+
import {
|
|
46
|
+
createManagementSessionControl,
|
|
47
|
+
initializeManagementAuthState,
|
|
48
|
+
issueGuiSession,
|
|
49
|
+
managementPrincipal,
|
|
50
|
+
requireManagementAuth,
|
|
51
|
+
type ManagementAuthState,
|
|
52
|
+
} from "../management-auth";
|
|
53
|
+
import { createReadinessGate, type ReadinessGate } from "../readiness";
|
|
54
|
+
import {
|
|
55
|
+
createRuntimePackageTreeIntegrityGuard,
|
|
56
|
+
type PackageTreeIntegrityGuard,
|
|
57
|
+
} from "../../lib/package-tree-integrity";
|
|
58
|
+
import type { LiveSidebandWebSocketFactory } from "./live-sideband";
|
|
59
|
+
|
|
60
|
+
// GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
|
|
61
|
+
// "../src/server" import surface stable for tests/callers.
|
|
62
|
+
|
|
63
|
+
// Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
|
|
64
|
+
|
|
65
|
+
// Source invariant for tests/responses/passthrough-abort.test.ts after the pure module split:
|
|
66
|
+
// if (isEventStream && upstreamResponse.body) {
|
|
67
|
+
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
68
|
+
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
69
|
+
// #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
|
|
70
|
+
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
|
|
71
|
+
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
|
|
72
|
+
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
73
|
+
// Codex upstream WS runtime gating and the forced bounded single-reader branch
|
|
74
|
+
// are owned by responses/ws-upstream.ts and responses/core.ts respectively.
|
|
75
|
+
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
76
|
+
// new Response(eagerBody,
|
|
77
|
+
// Default shape (tee + background inspection):
|
|
78
|
+
// upstreamResponse.body.tee()
|
|
79
|
+
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
80
|
+
// relaySseWithFailedTail(repairedBody, upstream)
|
|
81
|
+
// new Response(clientBody
|
|
82
|
+
// markNativePassthroughSseResponse
|
|
83
|
+
// const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
84
|
+
// function responseWithDeferredRequestLog
|
|
85
|
+
// isNativePassthroughSseResponse(response)
|
|
86
|
+
// trackSseForRequestLog(
|
|
87
|
+
// export function relaySseWithHeartbeat
|
|
88
|
+
|
|
89
|
+
const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id";
|
|
90
|
+
|
|
91
|
+
export function withRequestLogId(response: Response, requestId: string): Response {
|
|
92
|
+
const headers = new Headers(response.headers);
|
|
93
|
+
headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId);
|
|
94
|
+
// A custom `x-` header is not CORS-safelisted, so cross-origin JavaScript gets null from
|
|
95
|
+
// `response.headers.get()` even though the header is on the wire. Naming it here is what
|
|
96
|
+
// makes the id readable by a browser client — the only caller that needs a correlation id
|
|
97
|
+
// it did not send itself.
|
|
98
|
+
//
|
|
99
|
+
// Appending to whatever `withCors` already set, rather than overwriting, keeps this
|
|
100
|
+
// independent of the CORS layer: if the data plane later exposes another header, both
|
|
101
|
+
// survive. Duplicate names are harmless, and the header stays absent from responses that
|
|
102
|
+
// never reach this wrapper, so no management or rejected-origin response is widened.
|
|
103
|
+
const exposed = headers.get("Access-Control-Expose-Headers");
|
|
104
|
+
const already = (exposed ?? "")
|
|
105
|
+
.split(",")
|
|
106
|
+
.some(name => name.trim().toLowerCase() === REQUEST_LOG_ID_RESPONSE_HEADER);
|
|
107
|
+
if (!already) {
|
|
108
|
+
headers.set(
|
|
109
|
+
"Access-Control-Expose-Headers",
|
|
110
|
+
exposed ? `${exposed}, ${REQUEST_LOG_ID_RESPONSE_HEADER}` : REQUEST_LOG_ID_RESPONSE_HEADER,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return new Response(response.body, {
|
|
114
|
+
status: response.status,
|
|
115
|
+
statusText: response.statusText,
|
|
116
|
+
headers,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface StartServerDeps {
|
|
121
|
+
/** Test-only seam; production always initializes its own management credential state. */
|
|
122
|
+
managementAuthState?: ManagementAuthState;
|
|
123
|
+
/** Test-only route dependencies, forwarded only after management admission succeeds. */
|
|
124
|
+
managementApi?: ManagementApiDeps;
|
|
125
|
+
/** Test-only native-main recovery dependencies; production constructs the normal manager. */
|
|
126
|
+
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
127
|
+
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
128
|
+
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
129
|
+
/** Test-only service-home resolver; production resolves the current homes directly. */
|
|
130
|
+
resolveServiceHomes?: typeof currentServiceHomes;
|
|
131
|
+
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
132
|
+
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
133
|
+
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
134
|
+
localAttestationSecret?: string;
|
|
135
|
+
/** Optional readiness gate; a fresh pending gate is created when omitted. */
|
|
136
|
+
readinessGate?: ReadinessGate;
|
|
137
|
+
/** Test-only package-tree observation; production captures package.json identity at boot. */
|
|
138
|
+
packageTreeIntegrity?: PackageTreeIntegrityGuard;
|
|
139
|
+
/** Test-only seam for observing quota-worker registration ownership. */
|
|
140
|
+
registerCodexQuotaAutoRefreshWorker?: typeof registerCodexQuotaAutoRefreshWorker;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function inspectStartupOwnership(
|
|
144
|
+
deps: StartServerDeps,
|
|
145
|
+
currentHomes: ReturnType<typeof currentServiceHomes> | null,
|
|
146
|
+
statePaths: readonly string[] | null,
|
|
147
|
+
windowsTaskListingCache?: ReturnType<typeof createWindowsTaskListingCache>,
|
|
148
|
+
): OwnershipInspection {
|
|
149
|
+
try {
|
|
150
|
+
if (currentHomes === null || statePaths === null) {
|
|
151
|
+
return {
|
|
152
|
+
ownership: "unknown",
|
|
153
|
+
reason: "startup service-home resolution failed",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (deps.inspectNativeCodexOwnership) {
|
|
157
|
+
return deps.inspectNativeCodexOwnership({ currentHomes, statePaths, windowsTaskListingCache });
|
|
158
|
+
}
|
|
159
|
+
return inspectNativeCodexOwnership({ currentHomes, statePaths, windowsTaskListingCache });
|
|
160
|
+
} catch {
|
|
161
|
+
return {
|
|
162
|
+
ownership: "unknown",
|
|
163
|
+
reason: "service-home ownership inspection failed",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* #1046. `startServer` rewrites the Codex models cache during boot, and an
|
|
170
|
+
* app-server that started earlier keeps its own in-memory model list. The stale
|
|
171
|
+
* warning is not emitted here: `handleStart` runs a catalog sync moments later,
|
|
172
|
+
* so warning now would read an mtime that write is about to move, and both sites
|
|
173
|
+
* calling the helper independently would warn twice. This records the fact; the
|
|
174
|
+
* CLI start path owns the single decision.
|
|
175
|
+
*
|
|
176
|
+
* A caller that starts a server without `handleStart` (tests, embedded use)
|
|
177
|
+
* deliberately gets no warning — lifecycle diagnostics belong to whoever owns
|
|
178
|
+
* the lifecycle.
|
|
179
|
+
*/
|
|
180
|
+
let startupCacheInvalidationWrote = false;
|
|
181
|
+
|
|
182
|
+
/** #1046: did this process's startup cache invalidation actually write? */
|
|
183
|
+
/**
|
|
184
|
+
* The composition root owns WHEN the startup cache invalidation runs, but the flag lives here
|
|
185
|
+
* with its reader. An ES import binding is read-only, so the root cannot assign to it across
|
|
186
|
+
* the module boundary the way it did when both sides were one file. This setter is that
|
|
187
|
+
* assignment, kept next to the reader so the two cannot drift apart.
|
|
188
|
+
*/
|
|
189
|
+
export function setStartupCacheInvalidationWrite(wrote: boolean): void {
|
|
190
|
+
startupCacheInvalidationWrote = wrote;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function consumeStartupCacheInvalidationWrite(): boolean {
|
|
194
|
+
const wrote = startupCacheInvalidationWrote;
|
|
195
|
+
startupCacheInvalidationWrote = false;
|
|
196
|
+
return wrote;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function warnAgentTaskRecoveryStartup(config: {
|
|
200
|
+
agentTaskRecovery?: { enabled?: boolean };
|
|
201
|
+
}): void {
|
|
202
|
+
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
203
|
+
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
204
|
+
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
|
|
205
|
+
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function warnPlaintextV2AgentMessagesStartup(config: { plaintextV2AgentMessages?: boolean }): void {
|
|
209
|
+
if (config.plaintextV2AgentMessages !== true) return;
|
|
210
|
+
console.warn("⚠️ Experimental plaintext V2 agent messages are enabled.");
|
|
211
|
+
console.warn(" Eligible ChatGPT collaboration calls may carry plaintext message arguments. HTTPS remains encrypted, but task text may be retained in Codex history, selected providers, and local response/debug state.");
|
|
212
|
+
console.warn(" This depends on undocumented ChatGPT and Codex behavior; it does not decrypt existing tasks.");
|
|
213
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
+
import {
|
|
3
|
+
LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS,
|
|
4
|
+
MAX_WS_FRAME_BYTES,
|
|
5
|
+
WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
6
|
+
attachLiveSidebandUpstream,
|
|
7
|
+
closeLiveSideband,
|
|
8
|
+
closeLiveSidebandBeforeUpgrade,
|
|
9
|
+
enqueueLiveSidebandPendingFrame,
|
|
10
|
+
exceedsLiveSidebandFrameByteLimit,
|
|
11
|
+
openLiveSidebandUpstream,
|
|
12
|
+
sendUpstreamFrame,
|
|
13
|
+
webSocketFrameBytes,
|
|
14
|
+
} from "./live-sideband";
|
|
15
|
+
import { markActivity } from "../../lib/sidecar-tracker";
|
|
16
|
+
import {
|
|
17
|
+
buildWarmupCompletionFrames,
|
|
18
|
+
buildWsErrorFrame,
|
|
19
|
+
selectForwardHeaders,
|
|
20
|
+
sendJsonFrame,
|
|
21
|
+
buildResponsesWsData,
|
|
22
|
+
sendResponseToWebSocket,
|
|
23
|
+
sendTextFrame,
|
|
24
|
+
type WsData,
|
|
25
|
+
} from "../ws-bridge";
|
|
26
|
+
import {
|
|
27
|
+
CodexAccountCooldownError,
|
|
28
|
+
cooldownErrorMessage,
|
|
29
|
+
} from "../../codex/auth-context";
|
|
30
|
+
import { codexAccountNamespaceForModel } from "../../codex/account-namespace-match";
|
|
31
|
+
import {
|
|
32
|
+
registerCodexWebSocket,
|
|
33
|
+
tryReserveCodexWebSocket,
|
|
34
|
+
unregisterCodexWebSocket,
|
|
35
|
+
updateCodexWebSocketAuthContext,
|
|
36
|
+
} from "../../codex/websocket-registry";
|
|
37
|
+
import {
|
|
38
|
+
formatErrorResponse,
|
|
39
|
+
type ResponsesTerminalStatus,
|
|
40
|
+
} from "../../bridge";
|
|
41
|
+
import {
|
|
42
|
+
isDraining,
|
|
43
|
+
registerTurn,
|
|
44
|
+
tryAdmitTurn,
|
|
45
|
+
unregisterTurn,
|
|
46
|
+
type ActiveTurnLease,
|
|
47
|
+
} from "../lifecycle";
|
|
48
|
+
import {
|
|
49
|
+
addFinalRequestLog,
|
|
50
|
+
httpStatusForRequestLogTerminal,
|
|
51
|
+
inspectResponseLogSsePayload,
|
|
52
|
+
nextRequestLogId,
|
|
53
|
+
recordFirstOutput,
|
|
54
|
+
type RequestLogContext,
|
|
55
|
+
type RequestLogEntry,
|
|
56
|
+
} from "../request-log";
|
|
57
|
+
import {
|
|
58
|
+
corsHeaders,
|
|
59
|
+
managementCorsHeaders,
|
|
60
|
+
isAllowedRequestOrigin,
|
|
61
|
+
isAllowedManagementOrigin,
|
|
62
|
+
isApiAuthRequired,
|
|
63
|
+
jsonResponse,
|
|
64
|
+
admissionFields,
|
|
65
|
+
resolveApiAuth,
|
|
66
|
+
resolveResponsesApiAuth,
|
|
67
|
+
type RequestPolicyView,
|
|
68
|
+
withCors,
|
|
69
|
+
withManagementCors,
|
|
70
|
+
} from "../auth-cors";
|
|
71
|
+
import {
|
|
72
|
+
disableResponsesRequestTimeout,
|
|
73
|
+
handleResponses,
|
|
74
|
+
handleResponsesCompact,
|
|
75
|
+
} from "../responses";
|
|
76
|
+
import {
|
|
77
|
+
handleLive,
|
|
78
|
+
logLiveSidebandFrame,
|
|
79
|
+
parseLiveSidebandTarget,
|
|
80
|
+
resolveLiveSidebandUpgrade,
|
|
81
|
+
} from "../live";
|
|
82
|
+
import type { ServeOptionsContext } from "./serve-options";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
|
|
86
|
+
* under the 2,000-line ratchet threshold. The body is the original handler verbatim; it reads the
|
|
87
|
+
* same startServer context the HTTP half does, so it takes the same context object.
|
|
88
|
+
*/
|
|
89
|
+
export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
90
|
+
const { config, deps } = ctx;
|
|
91
|
+
return {
|
|
92
|
+
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
93
|
+
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
94
|
+
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
95
|
+
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
96
|
+
// Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
|
|
97
|
+
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
98
|
+
open(ws: ServerWebSocket<WsData>) {
|
|
99
|
+
if (ws.data.kind === "remote-workspace-agent") {
|
|
100
|
+
const open = ws.data.remoteWorkspaceOpen;
|
|
101
|
+
if (!open) {
|
|
102
|
+
ws.close(1011, "remote workspace connection unavailable");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
ws.data.remoteWorkspaceConnection = open(ws);
|
|
107
|
+
} catch {
|
|
108
|
+
ws.close(1011, "remote workspace connection failed");
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (ws.data.kind === "live-sideband") {
|
|
113
|
+
if (!ws.data.liveTurnAdmissionLease) {
|
|
114
|
+
closeLiveSideband(ws, 1013, "server busy");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!ws.data.admissionLease) {
|
|
121
|
+
ws.close(1013, "server busy");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
ws.data.admissionLease.bind(ws);
|
|
125
|
+
registerCodexWebSocket(ws);
|
|
126
|
+
},
|
|
127
|
+
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
128
|
+
if (ws.data.kind === "remote-workspace-agent") {
|
|
129
|
+
try {
|
|
130
|
+
ws.data.remoteWorkspaceConnection?.receive(raw);
|
|
131
|
+
} catch {
|
|
132
|
+
ws.close(1008, "remote workspace protocol error");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (ws.data.kind === "live-sideband") {
|
|
137
|
+
if (ws.data.liveClosing) return;
|
|
138
|
+
if (ws.data.liveValidateFrame && !ws.data.liveValidateFrame(raw)) {
|
|
139
|
+
closeLiveSideband(ws, 1008, "invalid audio event");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const rawBytes = webSocketFrameBytes(raw);
|
|
143
|
+
if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
|
|
144
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
logLiveSidebandFrame("c2u", raw);
|
|
148
|
+
const upstream = ws.data.liveUpstream;
|
|
149
|
+
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
150
|
+
const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
|
|
151
|
+
if (enqueueResult === "too-many-frames") {
|
|
152
|
+
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (enqueueResult === "too-many-bytes") {
|
|
156
|
+
closeLiveSideband(ws, 1009, "too many pending bytes");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (upstream.readyState !== WebSocket.OPEN) {
|
|
162
|
+
closeLiveSideband(ws, 1011, "upstream not open");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
sendUpstreamFrame(upstream, raw);
|
|
167
|
+
if (ws.data.liveMaxSessionMs !== undefined && upstream.bufferedAmount > MAX_WS_FRAME_BYTES) {
|
|
168
|
+
closeLiveSideband(ws, 1013, "audio upstream backpressure");
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const rawBytes = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength;
|
|
176
|
+
if (rawBytes > MAX_WS_FRAME_BYTES) {
|
|
177
|
+
sendJsonFrame(ws, buildWsErrorFrame(413, {
|
|
178
|
+
type: "invalid_request_error",
|
|
179
|
+
message: "WebSocket response.create frame is too large",
|
|
180
|
+
}));
|
|
181
|
+
ws.close(1009, "message too large");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let frame: Record<string, unknown>;
|
|
185
|
+
try {
|
|
186
|
+
frame = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as Record<string, unknown>;
|
|
187
|
+
} catch {
|
|
188
|
+
return; // text-only contract; ignore unparseable frames
|
|
189
|
+
}
|
|
190
|
+
if (frame.type === "response.processed") return; // ack — no-op
|
|
191
|
+
if (frame.type !== "response.create") return;
|
|
192
|
+
markActivity("ws response.create");
|
|
193
|
+
|
|
194
|
+
ws.data.cancel?.();
|
|
195
|
+
const turnId = (ws.data.turnId ?? 0) + 1;
|
|
196
|
+
ws.data.turnId = turnId;
|
|
197
|
+
const isCurrent = () => ws.data.turnId === turnId;
|
|
198
|
+
const turnAbort = new AbortController();
|
|
199
|
+
const cancelTurn = () => {
|
|
200
|
+
turnAbort.abort("websocket turn superseded or closed");
|
|
201
|
+
};
|
|
202
|
+
ws.data.cancel = cancelTurn;
|
|
203
|
+
// A socket may carry several response.create frames. Clear the previous
|
|
204
|
+
// account before resolving this frame so a failed Multi resolution cannot
|
|
205
|
+
// leave stale invalidation ownership behind.
|
|
206
|
+
updateCodexWebSocketAuthContext(ws, undefined);
|
|
207
|
+
|
|
208
|
+
if (frame.generate === false) {
|
|
209
|
+
for (const payload of buildWarmupCompletionFrames(frame)) {
|
|
210
|
+
if (!isCurrent()) return;
|
|
211
|
+
sendTextFrame(ws, payload);
|
|
212
|
+
}
|
|
213
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
|
|
218
|
+
if (!turnAdmissionLease) {
|
|
219
|
+
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
220
|
+
type: "server_error",
|
|
221
|
+
code: "server_busy",
|
|
222
|
+
message: "active turns capacity reached",
|
|
223
|
+
retryable: true,
|
|
224
|
+
}, new Headers({ "Retry-After": "1" })));
|
|
225
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const payload: Record<string, unknown> = { ...frame };
|
|
230
|
+
delete payload.type;
|
|
231
|
+
turnAdmissionLease.bindAbortController(turnAbort);
|
|
232
|
+
void (async () => {
|
|
233
|
+
const start = Date.now();
|
|
234
|
+
const requestId = nextRequestLogId(start);
|
|
235
|
+
// Resolved once at the handshake — a frame has no request headers left
|
|
236
|
+
// to re-resolve from. Optional on WsData like every other member, so
|
|
237
|
+
// narrow rather than assume: an unattributed frame is preferable to a
|
|
238
|
+
// fabricated attribution.
|
|
239
|
+
const wsAdmission = ws.data.admission;
|
|
240
|
+
const logCtx: RequestLogContext = {
|
|
241
|
+
model: "unknown",
|
|
242
|
+
provider: "unknown",
|
|
243
|
+
...(wsAdmission ? admissionFields(wsAdmission) : {}),
|
|
244
|
+
inboundProtocol: "responses",
|
|
245
|
+
};
|
|
246
|
+
let logged = false;
|
|
247
|
+
const finalizeLog = (
|
|
248
|
+
status: number,
|
|
249
|
+
meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
250
|
+
) => {
|
|
251
|
+
if (logged) return;
|
|
252
|
+
logged = true;
|
|
253
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
254
|
+
};
|
|
255
|
+
const baseHeaders = ws.data.headers ?? new Headers();
|
|
256
|
+
const fwd = new Headers({ "content-type": "application/json" });
|
|
257
|
+
baseHeaders.forEach((value, key) => fwd.set(key, value));
|
|
258
|
+
const req = new Request("http://localhost/v1/responses", {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: fwd,
|
|
261
|
+
body: JSON.stringify({ ...payload, stream: true }),
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
265
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
266
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
267
|
+
forceEmptyResponseId: true,
|
|
268
|
+
inboundTransport: "websocket",
|
|
269
|
+
abortSignal: turnAbort.signal,
|
|
270
|
+
turnAdmissionLease,
|
|
271
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
272
|
+
onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
|
|
273
|
+
recordTerminalOutcomes: false,
|
|
274
|
+
setTerminalOutcomeRecorder: recorder => {
|
|
275
|
+
terminalRecorder = recorder;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
279
|
+
onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
280
|
+
onTerminal: status => {
|
|
281
|
+
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
282
|
+
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
283
|
+
terminalStatus: status,
|
|
284
|
+
closeReason: "terminal",
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
if (!logged) finalizeLog(turnAbort.signal.aborted ? 499 : response.status);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (!isCurrent()) return;
|
|
291
|
+
try {
|
|
292
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
293
|
+
finalizeLog(429);
|
|
294
|
+
// Codex Desktop rides this WS transport, so it must carry the same
|
|
295
|
+
// actionable text as HTTP; a frame has no headers, hence message-only.
|
|
296
|
+
const accountSelector = typeof payload.model === "string"
|
|
297
|
+
? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
|
|
298
|
+
: undefined;
|
|
299
|
+
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
300
|
+
type: "rate_limit_error",
|
|
301
|
+
message: cooldownErrorMessage(err, accountSelector),
|
|
302
|
+
}));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
finalizeLog(502);
|
|
306
|
+
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
307
|
+
type: "proxy_error",
|
|
308
|
+
message: err instanceof Error ? err.message : String(err),
|
|
309
|
+
}));
|
|
310
|
+
} catch {
|
|
311
|
+
/* socket already gone or send dropped */
|
|
312
|
+
}
|
|
313
|
+
} finally {
|
|
314
|
+
turnAdmissionLease.release();
|
|
315
|
+
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|
|
316
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
},
|
|
320
|
+
close(ws: ServerWebSocket<WsData>) {
|
|
321
|
+
if (ws.data.kind === "remote-workspace-agent") {
|
|
322
|
+
ws.data.remoteWorkspaceClose?.();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (ws.data.kind === "live-sideband") {
|
|
326
|
+
closeLiveSideband(ws);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
unregisterCodexWebSocket(ws);
|
|
330
|
+
ws.data.admissionLease?.release();
|
|
331
|
+
ws.data.admissionLease = undefined;
|
|
332
|
+
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
333
|
+
},
|
|
334
|
+
} as const;
|
|
335
|
+
}
|