@bitkyc08/opencodex 2.55.0 → 2.57.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.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -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,339 @@
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
+ clientCloseForUpstream,
8
+ closeLiveSideband,
9
+ closeLiveSidebandBeforeUpgrade,
10
+ enqueueLiveSidebandPendingFrame,
11
+ exceedsLiveSidebandFrameByteLimit,
12
+ openLiveSidebandUpstream,
13
+ sendUpstreamFrame,
14
+ webSocketFrameBytes,
15
+ } from "./live-sideband";
16
+ import { markActivity } from "../../lib/sidecar-tracker";
17
+ import {
18
+ buildWarmupCompletionFrames,
19
+ buildWsErrorFrame,
20
+ selectForwardHeaders,
21
+ sendJsonFrame,
22
+ buildResponsesWsData,
23
+ sendResponseToWebSocket,
24
+ sendTextFrame,
25
+ type WsData,
26
+ } from "../ws-bridge";
27
+ import {
28
+ CodexAccountCooldownError,
29
+ cooldownErrorMessage,
30
+ } from "../../codex/auth-context";
31
+ import { codexAccountNamespaceForModel } from "../../codex/account-namespace-match";
32
+ import {
33
+ registerCodexWebSocket,
34
+ tryReserveCodexWebSocket,
35
+ unregisterCodexWebSocket,
36
+ updateCodexWebSocketAuthContext,
37
+ } from "../../codex/websocket-registry";
38
+ import {
39
+ formatErrorResponse,
40
+ type ResponsesTerminalStatus,
41
+ } from "../../bridge";
42
+ import {
43
+ isDraining,
44
+ registerTurn,
45
+ tryAdmitTurn,
46
+ unregisterTurn,
47
+ type ActiveTurnLease,
48
+ } from "../lifecycle";
49
+ import {
50
+ addFinalRequestLog,
51
+ httpStatusForRequestLogTerminal,
52
+ inspectResponseLogSsePayload,
53
+ nextRequestLogId,
54
+ recordFirstOutput,
55
+ type RequestLogContext,
56
+ type RequestLogEntry,
57
+ } from "../request-log";
58
+ import {
59
+ corsHeaders,
60
+ managementCorsHeaders,
61
+ isAllowedRequestOrigin,
62
+ isAllowedManagementOrigin,
63
+ isApiAuthRequired,
64
+ jsonResponse,
65
+ admissionFields,
66
+ resolveApiAuth,
67
+ resolveResponsesApiAuth,
68
+ type RequestPolicyView,
69
+ withCors,
70
+ withManagementCors,
71
+ } from "../auth-cors";
72
+ import {
73
+ disableResponsesRequestTimeout,
74
+ handleResponses,
75
+ handleResponsesCompact,
76
+ } from "../responses";
77
+ import {
78
+ handleLive,
79
+ logLiveSidebandFrame,
80
+ parseLiveSidebandTarget,
81
+ resolveLiveSidebandUpgrade,
82
+ } from "../live";
83
+ import type { ServeOptionsContext } from "./serve-options";
84
+
85
+ /**
86
+ * The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
87
+ * under the 2,000-line ratchet threshold. The body is the original handler verbatim; it reads the
88
+ * same startServer context the HTTP half does, so it takes the same context object.
89
+ */
90
+ export function createWebsocketHandler(ctx: ServeOptionsContext) {
91
+ const { config, deps } = ctx;
92
+ return {
93
+ maxPayloadLength: MAX_WS_FRAME_BYTES,
94
+ idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
95
+ // Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
96
+ // socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
97
+ // Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
98
+ // Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
99
+ open(ws: ServerWebSocket<WsData>) {
100
+ if (ws.data.kind === "remote-workspace-agent") {
101
+ const open = ws.data.remoteWorkspaceOpen;
102
+ if (!open) {
103
+ ws.close(1011, "remote workspace connection unavailable");
104
+ return;
105
+ }
106
+ try {
107
+ ws.data.remoteWorkspaceConnection = open(ws);
108
+ } catch {
109
+ ws.close(1011, "remote workspace connection failed");
110
+ }
111
+ return;
112
+ }
113
+ if (ws.data.kind === "live-sideband") {
114
+ if (!ws.data.liveTurnAdmissionLease) {
115
+ closeLiveSideband(ws, 1013, "server busy");
116
+ return;
117
+ }
118
+ attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
119
+ return;
120
+ }
121
+ if (!ws.data.admissionLease) {
122
+ ws.close(1013, "server busy");
123
+ return;
124
+ }
125
+ ws.data.admissionLease.bind(ws);
126
+ registerCodexWebSocket(ws);
127
+ },
128
+ message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
129
+ if (ws.data.kind === "remote-workspace-agent") {
130
+ try {
131
+ ws.data.remoteWorkspaceConnection?.receive(raw);
132
+ } catch {
133
+ ws.close(1008, "remote workspace protocol error");
134
+ }
135
+ return;
136
+ }
137
+ if (ws.data.kind === "live-sideband") {
138
+ if (ws.data.liveClosing) return;
139
+ if (ws.data.liveValidateFrame && !ws.data.liveValidateFrame(raw)) {
140
+ closeLiveSideband(ws, 1008, "invalid audio event");
141
+ return;
142
+ }
143
+ const rawBytes = webSocketFrameBytes(raw);
144
+ if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
145
+ closeLiveSideband(ws, 1009, "message too large");
146
+ return;
147
+ }
148
+ logLiveSidebandFrame("c2u", raw);
149
+ const upstream = ws.data.liveUpstream;
150
+ if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
151
+ const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
152
+ if (enqueueResult === "too-many-frames") {
153
+ closeLiveSideband(ws, 1009, "too many pending frames");
154
+ return;
155
+ }
156
+ if (enqueueResult === "too-many-bytes") {
157
+ closeLiveSideband(ws, 1009, "too many pending bytes");
158
+ return;
159
+ }
160
+ return;
161
+ }
162
+ if (upstream.readyState !== WebSocket.OPEN) {
163
+ closeLiveSideband(ws, 1011, "upstream not open");
164
+ return;
165
+ }
166
+ try {
167
+ sendUpstreamFrame(upstream, raw);
168
+ if (ws.data.liveMaxSessionMs !== undefined && upstream.bufferedAmount > MAX_WS_FRAME_BYTES) {
169
+ closeLiveSideband(ws, 1013, "audio upstream backpressure");
170
+ }
171
+ } catch {
172
+ closeLiveSideband(ws, 1011, "upstream send failed");
173
+ }
174
+ return;
175
+ }
176
+ const rawBytes = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength;
177
+ if (rawBytes > MAX_WS_FRAME_BYTES) {
178
+ sendJsonFrame(ws, buildWsErrorFrame(413, {
179
+ type: "invalid_request_error",
180
+ message: "WebSocket response.create frame is too large",
181
+ }));
182
+ ws.close(1009, "message too large");
183
+ return;
184
+ }
185
+ let frame: Record<string, unknown>;
186
+ try {
187
+ frame = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as Record<string, unknown>;
188
+ } catch {
189
+ return; // text-only contract; ignore unparseable frames
190
+ }
191
+ if (frame.type === "response.processed") return; // ack — no-op
192
+ if (frame.type !== "response.create") return;
193
+ markActivity("ws response.create");
194
+
195
+ ws.data.cancel?.();
196
+ const turnId = (ws.data.turnId ?? 0) + 1;
197
+ ws.data.turnId = turnId;
198
+ const isCurrent = () => ws.data.turnId === turnId;
199
+ const turnAbort = new AbortController();
200
+ const cancelTurn = () => {
201
+ turnAbort.abort("websocket turn superseded or closed");
202
+ };
203
+ ws.data.cancel = cancelTurn;
204
+ // A socket may carry several response.create frames. Clear the previous
205
+ // account before resolving this frame so a failed Multi resolution cannot
206
+ // leave stale invalidation ownership behind.
207
+ updateCodexWebSocketAuthContext(ws, undefined);
208
+
209
+ if (frame.generate === false) {
210
+ for (const payload of buildWarmupCompletionFrames(frame)) {
211
+ if (!isCurrent()) return;
212
+ sendTextFrame(ws, payload);
213
+ }
214
+ if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
215
+ return;
216
+ }
217
+
218
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
219
+ if (!turnAdmissionLease) {
220
+ sendJsonFrame(ws, buildWsErrorFrame(503, {
221
+ type: "server_error",
222
+ code: "server_busy",
223
+ message: "active turns capacity reached",
224
+ retryable: true,
225
+ }, new Headers({ "Retry-After": "1" })));
226
+ if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
227
+ return;
228
+ }
229
+
230
+ const payload: Record<string, unknown> = { ...frame };
231
+ delete payload.type;
232
+ turnAdmissionLease.bindAbortController(turnAbort);
233
+ void (async () => {
234
+ const start = Date.now();
235
+ const requestId = nextRequestLogId(start);
236
+ // Resolved once at the handshake — a frame has no request headers left
237
+ // to re-resolve from. Optional on WsData like every other member, so
238
+ // narrow rather than assume: an unattributed frame is preferable to a
239
+ // fabricated attribution.
240
+ const wsAdmission = ws.data.admission;
241
+ const logCtx: RequestLogContext = {
242
+ model: "unknown",
243
+ provider: "unknown",
244
+ ...(wsAdmission ? admissionFields(wsAdmission) : {}),
245
+ inboundProtocol: "responses",
246
+ };
247
+ let logged = false;
248
+ const finalizeLog = (
249
+ status: number,
250
+ meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
251
+ ) => {
252
+ if (logged) return;
253
+ logged = true;
254
+ addFinalRequestLog(requestId, start, logCtx, status, meta);
255
+ };
256
+ const baseHeaders = ws.data.headers ?? new Headers();
257
+ const fwd = new Headers({ "content-type": "application/json" });
258
+ baseHeaders.forEach((value, key) => fwd.set(key, value));
259
+ const req = new Request("http://localhost/v1/responses", {
260
+ method: "POST",
261
+ headers: fwd,
262
+ body: JSON.stringify({ ...payload, stream: true }),
263
+ });
264
+ try {
265
+ let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
266
+ const response = await handleResponses(req, config, logCtx, {
267
+ ...(wsAdmission ? { admission: wsAdmission } : {}),
268
+ forceEmptyResponseId: true,
269
+ inboundTransport: "websocket",
270
+ abortSignal: turnAbort.signal,
271
+ turnAdmissionLease,
272
+ onFirstOutput: () => recordFirstOutput(logCtx, start),
273
+ onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
274
+ recordTerminalOutcomes: false,
275
+ setTerminalOutcomeRecorder: recorder => {
276
+ terminalRecorder = recorder;
277
+ },
278
+ });
279
+ await sendResponseToWebSocket(ws, response, isCurrent, {
280
+ onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
281
+ onTerminal: status => {
282
+ terminalRecorder?.(status, logCtx.terminalHttpStatus);
283
+ finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
284
+ terminalStatus: status,
285
+ closeReason: "terminal",
286
+ });
287
+ },
288
+ });
289
+ if (!logged) finalizeLog(turnAbort.signal.aborted ? 499 : response.status);
290
+ } catch (err) {
291
+ if (!isCurrent()) return;
292
+ try {
293
+ if (err instanceof CodexAccountCooldownError) {
294
+ finalizeLog(429);
295
+ // Codex Desktop rides this WS transport, so it must carry the same
296
+ // actionable text as HTTP; a frame has no headers, hence message-only.
297
+ const accountSelector = typeof payload.model === "string"
298
+ ? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
299
+ : undefined;
300
+ sendJsonFrame(ws, buildWsErrorFrame(429, {
301
+ type: "rate_limit_error",
302
+ message: cooldownErrorMessage(err, accountSelector),
303
+ }));
304
+ return;
305
+ }
306
+ finalizeLog(502);
307
+ sendJsonFrame(ws, buildWsErrorFrame(502, {
308
+ type: "proxy_error",
309
+ message: err instanceof Error ? err.message : String(err),
310
+ }));
311
+ } catch {
312
+ /* socket already gone or send dropped */
313
+ }
314
+ } finally {
315
+ turnAdmissionLease.release();
316
+ if (!logged && turnAbort.signal.aborted) finalizeLog(499);
317
+ if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
318
+ }
319
+ })();
320
+ },
321
+ close(ws: ServerWebSocket<WsData>, code: number, reason: string) {
322
+ if (ws.data.kind === "remote-workspace-agent") {
323
+ ws.data.remoteWorkspaceClose?.();
324
+ return;
325
+ }
326
+ if (ws.data.kind === "live-sideband") {
327
+ // Carry the client's own close through to the upstream instead of reporting every
328
+ // hang-up as a plain 1000.
329
+ const forwarded = clientCloseForUpstream(code, reason);
330
+ closeLiveSideband(ws, forwarded.code, forwarded.reason);
331
+ return;
332
+ }
333
+ unregisterCodexWebSocket(ws);
334
+ ws.data.admissionLease?.release();
335
+ ws.data.admissionLease = undefined;
336
+ ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
337
+ },
338
+ } as const;
339
+ }