@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,343 @@
1
+ import net, { type Socket } from "node:net";
2
+
3
+ const DIRECT_LOCAL_HTTP_MAX_BYTES = 8 * 1024 * 1024;
4
+ const DIRECT_LOCAL_HTTP_TIMEOUT_MS = 10_000;
5
+
6
+ type DirectLocalHttpIo = {
7
+ timeoutMs?: number;
8
+ connect?: (hostname: string, port: number) => Socket;
9
+ };
10
+
11
+ function abortReason(signal: AbortSignal): Error {
12
+ if (signal.reason instanceof Error) return signal.reason;
13
+ const error = new Error("direct local HTTP request aborted");
14
+ error.name = "AbortError";
15
+ return error;
16
+ }
17
+
18
+ function headerBoundary(bytes: Buffer): number {
19
+ return bytes.indexOf("\r\n\r\n");
20
+ }
21
+
22
+ function validateTrailerLines(bytes: Buffer, start: number, end: number): void {
23
+ if (end === start) return;
24
+ for (const line of bytes.subarray(start, end).toString("latin1").split("\r\n")) {
25
+ if (line.indexOf(":") <= 0) {
26
+ throw new Error("direct local HTTP response has an invalid chunk trailer");
27
+ }
28
+ }
29
+ }
30
+
31
+ function decodeChunkedBody(body: Buffer): Buffer {
32
+ const chunks: Buffer[] = [];
33
+ let offset = 0;
34
+ for (;;) {
35
+ const lineEnd = body.indexOf("\r\n", offset);
36
+ if (lineEnd < 0) throw new Error("direct local HTTP response has a truncated chunk header");
37
+ const rawSize = body.subarray(offset, lineEnd).toString("ascii").split(";", 1)[0]?.trim() ?? "";
38
+ if (!/^[0-9a-f]+$/i.test(rawSize)) throw new Error("direct local HTTP response has an invalid chunk size");
39
+ const size = Number.parseInt(rawSize, 16);
40
+ offset = lineEnd + 2;
41
+ if (size === 0) {
42
+ if (body.length < offset + 2) throw new Error("direct local HTTP response has a truncated chunk trailer");
43
+ if (body[offset] !== 13 || body[offset + 1] !== 10) {
44
+ const trailerEnd = body.indexOf("\r\n\r\n", offset);
45
+ if (trailerEnd < 0) {
46
+ throw new Error("direct local HTTP response has a truncated chunk trailer");
47
+ }
48
+ validateTrailerLines(body, offset, trailerEnd);
49
+ if (trailerEnd + 4 !== body.length) {
50
+ throw new Error("direct local HTTP response has trailing bytes");
51
+ }
52
+ } else if (offset + 2 !== body.length) {
53
+ throw new Error("direct local HTTP response has trailing bytes");
54
+ }
55
+ return Buffer.concat(chunks);
56
+ }
57
+ if (!Number.isSafeInteger(size) || offset + size + 2 > body.length) {
58
+ throw new Error("direct local HTTP response has a truncated chunk body");
59
+ }
60
+ chunks.push(body.subarray(offset, offset + size));
61
+ offset += size;
62
+ if (body[offset] !== 13 || body[offset + 1] !== 10) {
63
+ throw new Error("direct local HTTP response has an invalid chunk terminator");
64
+ }
65
+ offset += 2;
66
+ }
67
+ }
68
+
69
+ type ResponseFraming =
70
+ | { kind: "head"; searchFrom: number }
71
+ | { kind: "content-length"; totalBytes: number }
72
+ | { kind: "chunk-size"; offset: number; searchFrom: number }
73
+ | { kind: "chunk-body"; offset: number; size: number }
74
+ | { kind: "trailers"; offset: number; searchFrom: number }
75
+ | { kind: "close" }
76
+ | { kind: "complete" };
77
+
78
+ function parseResponseHead(bytes: Buffer, boundary: number): {
79
+ status: number;
80
+ statusText: string;
81
+ headers: Headers;
82
+ } {
83
+ const lines = bytes.subarray(0, boundary).toString("latin1").split("\r\n");
84
+ const statusLine = lines.shift() ?? "";
85
+ const match = /^HTTP\/1\.[01] ([0-9]{3})(?: (.*))?$/.exec(statusLine);
86
+ if (!match) throw new Error("direct local HTTP response has an invalid status line");
87
+ const status = Number(match[1]);
88
+ if (status < 200 || status > 599) throw new Error("direct local HTTP response has an unsupported status");
89
+ const headers = new Headers();
90
+ for (const line of lines) {
91
+ const colon = line.indexOf(":");
92
+ if (colon <= 0) throw new Error("direct local HTTP response has an invalid header");
93
+ headers.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
94
+ }
95
+ return { status, statusText: match[2] ?? "", headers };
96
+ }
97
+
98
+ function advanceResponseFraming(bytes: Buffer, initial: ResponseFraming): ResponseFraming {
99
+ let framing = initial;
100
+ for (;;) {
101
+ if (framing.kind === "complete" || framing.kind === "close") return framing;
102
+ if (framing.kind === "head") {
103
+ const boundary = bytes.indexOf("\r\n\r\n", framing.searchFrom);
104
+ if (boundary < 0) {
105
+ if (bytes.length > 64 * 1024) throw new Error("direct local HTTP response headers exceed the byte cap");
106
+ return { kind: "head", searchFrom: Math.max(0, bytes.length - 3) };
107
+ }
108
+ const { status, headers } = parseResponseHead(bytes, boundary);
109
+ const bodyStart = boundary + 4;
110
+ if (status === 204 || status === 205 || status === 304) {
111
+ if (bytes.length > bodyStart) {
112
+ throw new Error("direct local HTTP response has a body for a bodyless status");
113
+ }
114
+ return { kind: "complete" };
115
+ }
116
+ if (/\bchunked\b/i.test(headers.get("transfer-encoding") ?? "")) {
117
+ framing = { kind: "chunk-size", offset: bodyStart, searchFrom: bodyStart };
118
+ continue;
119
+ }
120
+ const rawLength = headers.get("content-length");
121
+ if (rawLength === null) return { kind: "close" };
122
+ if (!/^[0-9]+$/.test(rawLength)) throw new Error("direct local HTTP response has an invalid content length");
123
+ const length = Number(rawLength);
124
+ const totalBytes = bodyStart + length;
125
+ if (!Number.isSafeInteger(length) || totalBytes > DIRECT_LOCAL_HTTP_MAX_BYTES) {
126
+ throw new Error("direct local HTTP response exceeds the byte cap");
127
+ }
128
+ framing = { kind: "content-length", totalBytes };
129
+ continue;
130
+ }
131
+ if (framing.kind === "content-length") {
132
+ if (bytes.length > framing.totalBytes) {
133
+ throw new Error("direct local HTTP response has trailing bytes");
134
+ }
135
+ return bytes.length === framing.totalBytes ? { kind: "complete" } : framing;
136
+ }
137
+ if (framing.kind === "chunk-size") {
138
+ const lineEnd = bytes.indexOf("\r\n", framing.searchFrom);
139
+ if (lineEnd < 0) {
140
+ if (bytes.length - framing.offset > 8 * 1024) {
141
+ throw new Error("direct local HTTP response chunk header exceeds the byte cap");
142
+ }
143
+ return {
144
+ ...framing,
145
+ searchFrom: Math.max(framing.offset, bytes.length - 1),
146
+ };
147
+ }
148
+ const rawSize = bytes.subarray(framing.offset, lineEnd).toString("ascii").split(";", 1)[0]?.trim() ?? "";
149
+ if (!/^[0-9a-f]+$/i.test(rawSize)) throw new Error("direct local HTTP response has an invalid chunk size");
150
+ const size = Number.parseInt(rawSize, 16);
151
+ if (!Number.isSafeInteger(size) || size > DIRECT_LOCAL_HTTP_MAX_BYTES) {
152
+ throw new Error("direct local HTTP response chunk exceeds the byte cap");
153
+ }
154
+ const offset = lineEnd + 2;
155
+ framing = size === 0
156
+ ? { kind: "trailers", offset, searchFrom: offset }
157
+ : { kind: "chunk-body", offset, size };
158
+ continue;
159
+ }
160
+ if (framing.kind === "chunk-body") {
161
+ const terminator = framing.offset + framing.size;
162
+ if (terminator + 2 > bytes.length) return framing;
163
+ if (bytes[terminator] !== 13 || bytes[terminator + 1] !== 10) {
164
+ throw new Error("direct local HTTP response has an invalid chunk terminator");
165
+ }
166
+ framing = { kind: "chunk-size", offset: terminator + 2, searchFrom: terminator + 2 };
167
+ continue;
168
+ }
169
+ if (bytes.length < framing.offset + 2) return framing;
170
+ if (bytes[framing.offset] === 13 && bytes[framing.offset + 1] === 10) {
171
+ if (bytes.length > framing.offset + 2) {
172
+ throw new Error("direct local HTTP response has trailing bytes");
173
+ }
174
+ return { kind: "complete" };
175
+ }
176
+ const trailerEnd = bytes.indexOf("\r\n\r\n", framing.searchFrom);
177
+ if (trailerEnd >= 0) {
178
+ validateTrailerLines(bytes, framing.offset, trailerEnd);
179
+ if (bytes.length > trailerEnd + 4) {
180
+ throw new Error("direct local HTTP response has trailing bytes");
181
+ }
182
+ return { kind: "complete" };
183
+ }
184
+ if (bytes.length - framing.offset > 64 * 1024) {
185
+ throw new Error("direct local HTTP response trailers exceed the byte cap");
186
+ }
187
+ return { ...framing, searchFrom: Math.max(framing.offset, bytes.length - 3) };
188
+ }
189
+ }
190
+
191
+ function parseResponse(bytes: Buffer): Response {
192
+ const boundary = headerBoundary(bytes);
193
+ if (boundary < 0) throw new Error("direct local HTTP response has no header boundary");
194
+ const { status, statusText, headers } = parseResponseHead(bytes, boundary);
195
+
196
+ let body = bytes.subarray(boundary + 4);
197
+ if (/\bchunked\b/i.test(headers.get("transfer-encoding") ?? "")) {
198
+ body = decodeChunkedBody(body);
199
+ headers.delete("transfer-encoding");
200
+ headers.delete("content-length");
201
+ } else {
202
+ const rawLength = headers.get("content-length");
203
+ if (rawLength !== null) {
204
+ if (!/^[0-9]+$/.test(rawLength)) throw new Error("direct local HTTP response has an invalid content length");
205
+ const length = Number(rawLength);
206
+ if (!Number.isSafeInteger(length) || body.byteLength < length) {
207
+ throw new Error("direct local HTTP response body is truncated");
208
+ }
209
+ body = body.subarray(0, length);
210
+ }
211
+ }
212
+
213
+ const bodyless = status === 204 || status === 205 || status === 304;
214
+ return new Response(bodyless ? null : new Uint8Array(body), {
215
+ status,
216
+ statusText,
217
+ headers,
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Fetch one local HTTP GET over a direct TCP connection.
223
+ *
224
+ * Bun's global fetch and Bun 1.3's node:http compatibility layer can honor
225
+ * HTTP(S)_PROXY. Local identity and capability probes must not expose headers
226
+ * to, or accept a fabricated response from, such a proxy. node:net connects to
227
+ * the selected listener without consulting proxy environment variables. Each
228
+ * caller retains an injected fetch seam for deterministic unit tests.
229
+ */
230
+ export async function directLocalHttpFetch(
231
+ input: string | URL | Request,
232
+ init: RequestInit = {},
233
+ io: DirectLocalHttpIo = {},
234
+ ): Promise<Response> {
235
+ const url = new URL(input instanceof Request ? input.url : String(input));
236
+ const method = (init.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
237
+ const signal = init.signal ?? (input instanceof Request ? input.signal : undefined);
238
+ const body = init.body ?? (input instanceof Request ? input.body : null);
239
+
240
+ if (url.protocol !== "http:") throw new Error("direct local request must use HTTP");
241
+ if (url.username || url.password) throw new Error("direct local request URL must not contain credentials");
242
+ if (method !== "GET" || body !== null) throw new Error("direct local request must be a bodyless GET");
243
+ if (signal?.aborted) throw abortReason(signal);
244
+
245
+ const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
246
+ headers.delete("proxy-authorization");
247
+ headers.delete("proxy-connection");
248
+ headers.set("host", url.host);
249
+ headers.set("connection", "close");
250
+ const headerLines: string[] = [];
251
+ headers.forEach((value, key) => { headerLines.push(`${key}: ${value}`); });
252
+ const requestBytes = Buffer.from(
253
+ `GET ${url.pathname}${url.search} HTTP/1.1\r\n${headerLines.join("\r\n")}\r\n\r\n`,
254
+ "latin1",
255
+ );
256
+ const parsedHostname = url.hostname.startsWith("[") && url.hostname.endsWith("]")
257
+ ? url.hostname.slice(1, -1)
258
+ : url.hostname;
259
+ const hostname = parsedHostname;
260
+ const port = url.port ? Number(url.port) : 80;
261
+ const timeoutMs = io.timeoutMs ?? DIRECT_LOCAL_HTTP_TIMEOUT_MS;
262
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
263
+ throw new Error("direct local HTTP timeout must be positive");
264
+ }
265
+
266
+ return await new Promise<Response>((resolve, reject) => {
267
+ let socket: Socket | undefined;
268
+ let settled = false;
269
+ let receivedBytes = 0;
270
+ let responseBytes = Buffer.allocUnsafe(4 * 1024);
271
+ let framing: ResponseFraming = { kind: "head", searchFrom: 0 };
272
+ const cleanup = () => {
273
+ signal?.removeEventListener("abort", onAbort);
274
+ socket?.setTimeout(0);
275
+ };
276
+ const finish = (error?: Error) => {
277
+ if (settled) return;
278
+ settled = true;
279
+ cleanup();
280
+ try { socket?.destroy(); } catch { /* ignore */ }
281
+ if (error) {
282
+ reject(error);
283
+ return;
284
+ }
285
+ try {
286
+ resolve(parseResponse(responseBytes.subarray(0, receivedBytes)));
287
+ } catch (parseError) {
288
+ reject(parseError instanceof Error ? parseError : new Error(String(parseError)));
289
+ }
290
+ };
291
+ const onAbort = () => {
292
+ const error = signal ? abortReason(signal) : new Error("direct local HTTP request aborted");
293
+ finish(error);
294
+ };
295
+
296
+ socket = (io.connect ?? ((host, selectedPort) => net.createConnection({
297
+ host,
298
+ port: selectedPort,
299
+ autoSelectFamily: true,
300
+ })))(hostname, port);
301
+ socket.setTimeout(timeoutMs, () => {
302
+ const error = new Error("direct local HTTP request timed out");
303
+ error.name = "TimeoutError";
304
+ finish(error);
305
+ });
306
+ signal?.addEventListener("abort", onAbort, { once: true });
307
+ if (signal?.aborted) {
308
+ onAbort();
309
+ return;
310
+ }
311
+ socket.on("connect", () => {
312
+ try { socket?.write(requestBytes); } catch (error) {
313
+ finish(error instanceof Error ? error : new Error(String(error)));
314
+ }
315
+ });
316
+ socket.on("data", chunk => {
317
+ if (settled) return;
318
+ const bytes = Buffer.from(chunk);
319
+ receivedBytes += bytes.byteLength;
320
+ if (receivedBytes > DIRECT_LOCAL_HTTP_MAX_BYTES) {
321
+ finish(new Error("direct local HTTP response exceeds the byte cap"));
322
+ return;
323
+ }
324
+ if (receivedBytes > responseBytes.byteLength) {
325
+ let capacity = responseBytes.byteLength;
326
+ while (capacity < receivedBytes) capacity = Math.min(DIRECT_LOCAL_HTTP_MAX_BYTES, capacity * 2);
327
+ const grown = Buffer.allocUnsafe(capacity);
328
+ responseBytes.copy(grown);
329
+ responseBytes = grown;
330
+ }
331
+ bytes.copy(responseBytes, receivedBytes - bytes.byteLength);
332
+ try {
333
+ framing = advanceResponseFraming(responseBytes.subarray(0, receivedBytes), framing);
334
+ if (framing.kind === "complete") finish();
335
+ } catch (error) {
336
+ finish(error instanceof Error ? error : new Error(String(error)));
337
+ }
338
+ });
339
+ socket.once("end", () => finish());
340
+ socket.once("error", error => finish(error));
341
+ socket.once("close", () => finish());
342
+ });
343
+ }
@@ -16,6 +16,7 @@ import {
16
16
  armClaudeCodeBaseline,
17
17
  loadConfig,
18
18
  saveConfig,
19
+ getConfigDir,
19
20
  websocketsEnabled,
20
21
  } from "../config";
21
22
  import { reconcileOAuthProviders } from "../oauth";
@@ -23,7 +24,10 @@ import { withCatalogWriteSerialization } from "../codex/catalog-write-serializat
23
24
  import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
24
25
  import { getCodexHome } from "../codex/paths";
25
26
  import { shouldSyncCodexOnStart } from "../codex/desired-state";
26
- import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
27
+ import {
28
+ inspectNativeCodexOwnership,
29
+ type OwnershipInspection,
30
+ } from "../integrations/native/ownership-preflight";
27
31
  import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
28
32
  import {
29
33
  reconcileLiveStateStores,
@@ -41,9 +45,16 @@ import {
41
45
  registerDefaultAppOwnedObservedBuffers,
42
46
  } from "../lib/app-owned-memory-stores";
43
47
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
48
+ import {
49
+ setLabAutomationDispatchDeps,
50
+ startLabAutomationScheduler,
51
+ } from "../lab/automation/orchestrator";
52
+ import { loadLabAutomationPolicy } from "../lab/automation/persistence";
53
+ import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
44
54
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
45
55
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
46
- import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
56
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
57
+ import { providerContextCap } from "../providers/context-cap";
47
58
  import { providerCodexAccountMode } from "../providers/registry";
48
59
  import type { StorageCleanupPolicy } from "../types";
49
60
  import {
@@ -89,7 +100,6 @@ import {
89
100
  addFinalRequestLog,
90
101
  hydrateRequestLogsFromDisk,
91
102
  httpStatusForRequestLogTerminal,
92
- httpStatusForTerminalStatus,
93
103
  inspectResponseLogSsePayload,
94
104
  nextRequestLogId,
95
105
  recordFirstOutput,
@@ -161,6 +171,7 @@ import { buildDesktop3pRegistry } from "../claude/desktop-3p";
161
171
  import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
162
172
  import {
163
173
  bindNativeMainStartupLifecycle,
174
+ blockNativeMainStartupForUnownedServiceHome,
164
175
  releaseNativeMainStartupLifecycle,
165
176
  startNativeMainStartupLifecycle,
166
177
  type NativeMainStartupGateDeps,
@@ -425,6 +436,8 @@ export interface StartServerDeps {
425
436
  managementApi?: ManagementApiDeps;
426
437
  /** Test-only native-main recovery dependencies; production constructs the normal manager. */
427
438
  nativeMainStartup?: NativeMainStartupGateDeps;
439
+ /** Test-only ownership evidence; production inspects the installed service state. */
440
+ inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
428
441
  /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
429
442
  liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
430
443
  /** Test-only seam; production derives a fresh local-attestation secret per process. */
@@ -433,6 +446,17 @@ export interface StartServerDeps {
433
446
  readinessGate?: ReadinessGate;
434
447
  }
435
448
 
449
+ function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
450
+ try {
451
+ return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
452
+ } catch {
453
+ return {
454
+ ownership: "unknown",
455
+ reason: "service-home ownership inspection failed",
456
+ };
457
+ }
458
+ }
459
+
436
460
  /*
437
461
  * #1046. `startServer` rewrites the Codex models cache during boot, and an
438
462
  * app-server that started earlier keeps its own in-memory model list. The stale
@@ -497,21 +521,28 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
497
521
  if (migrated) saveConfig(config);
498
522
  }
499
523
  }
524
+ // Resolve unattended service-home authority before any Codex lock, cache, owner,
525
+ // journal, or credential path. Both positive foreign evidence and an unprovable
526
+ // ownership state are non-authority.
527
+ startupCacheInvalidationWrote = false;
528
+ const startupCacheOwnership = inspectStartupOwnership(deps);
500
529
  // Startup cache invalidation is best-effort and must never block the server from
501
530
  // serving. It now takes K so it cannot race a convergence commit, but both the
502
531
  // home resolution and the acquisition can fail on a machine with no Codex home —
503
532
  // `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
504
533
  // otherwise turn "no Codex installed" into "proxy will not start".
505
- try {
506
- const startupCodexHome = getCodexHome();
507
- // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
508
- // with the later startup sync and warns ONCE about stale app-servers; warning
509
- // here instead would read a catalog mtime the sync is about to move.
510
- const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
511
- invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
512
- // A refused permit is not a write; only a completed run that returned true is.
513
- startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
514
- } catch { /* no readable Codex home: nothing to invalidate */ }
534
+ if (startupCacheOwnership.ownership === "owned") {
535
+ try {
536
+ const startupCodexHome = getCodexHome();
537
+ // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
538
+ // with the later startup sync and warns ONCE about stale app-servers; warning
539
+ // here instead would read a catalog mtime the sync is about to move.
540
+ const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
541
+ invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
542
+ // A refused permit is not a write; only a completed run that returned true is.
543
+ startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
544
+ } catch { /* no readable Codex home: nothing to invalidate */ }
545
+ }
515
546
  // Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
516
547
  // the server can serve a request, and AFTER the startup migrations above — those run
517
548
  // against a config nobody else holds and are the documented exception to the save
@@ -650,10 +681,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
650
681
  // CODEX_HOME. When the user has disabled the Codex integration, starting the
651
682
  // proxy must not manufacture those Codex artifacts merely to serve other
652
683
  // clients; no Codex request can use this lifecycle in that state.
653
- const nativeOwnership = inspectNativeCodexOwnership();
684
+ // Re-probe here instead of trusting the earlier cache decision: startup work
685
+ // between the two sites must not widen the service-install race.
686
+ const nativeOwnership = inspectStartupOwnership(deps);
654
687
  const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
655
- && nativeOwnership.ownership !== "foreign"
656
- ? startNativeMainStartupLifecycle(deps.nativeMainStartup)
688
+ ? nativeOwnership.ownership === "owned"
689
+ ? startNativeMainStartupLifecycle(deps.nativeMainStartup)
690
+ : blockNativeMainStartupForUnownedServiceHome(
691
+ nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
692
+ )
657
693
  : {
658
694
  homeId: null,
659
695
  settled: Promise.resolve({ status: "ready", homeId: null }),
@@ -911,7 +947,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
911
947
  const catalogNativeSlugs = accountSelectors.length > 0
912
948
  ? NATIVE_OPENAI_MODELS
913
949
  : nativeSlugs;
914
- const entries = buildCatalogEntries(loadCatalogTemplate(), catalogNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), accountSelectors, suppressedBareNativeSlugs);
950
+ const entries = buildCatalogEntries(
951
+ loadCatalogTemplate(),
952
+ catalogNativeSlugs,
953
+ goOrdered,
954
+ config.subagentModels,
955
+ websocketsEnabled(config),
956
+ maMode as "v1" | "default" | "v2",
957
+ exactComboCatalogSlugs(config),
958
+ accountSelectors,
959
+ suppressedBareNativeSlugs,
960
+ new Set(),
961
+ providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
962
+ );
915
963
  return jsonResponse({
916
964
  models: applyNativeVisibility(
917
965
  entries,
@@ -1136,7 +1184,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1136
1184
  abortSignal: req.signal,
1137
1185
  onFirstOutput: () => recordFirstOutput(logCtx, start),
1138
1186
  onNativePassthroughTerminal: status => {
1139
- finalizeNativePassthroughLog(httpStatusForTerminalStatus(status), {
1187
+ finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
1140
1188
  terminalStatus: status,
1141
1189
  closeReason: "terminal",
1142
1190
  });
@@ -1646,5 +1694,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1646
1694
  // Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
1647
1695
  backgroundLifecycle.scheduleStartupRun();
1648
1696
 
1697
+ const labConfigDir = getConfigDir();
1698
+ const productionLabRouteExecutor = createProductionLabRouteExecutor({
1699
+ configDir: labConfigDir,
1700
+ loadConfig: () => config,
1701
+ });
1702
+ setLabAutomationDispatchDeps({
1703
+ configDir: labConfigDir,
1704
+ loadConfig: () => config,
1705
+ routeExecutor: productionLabRouteExecutor,
1706
+ });
1707
+ if (loadLabAutomationPolicy(labConfigDir).enabled) {
1708
+ startLabAutomationScheduler(labConfigDir);
1709
+ }
1710
+
1649
1711
  return server;
1650
1712
  }
@@ -1,3 +1,4 @@
1
+ import { flushAntigravityReplay } from "../adapters/google-antigravity-replay";
1
2
  import { flushResponseState } from "../responses/state";
2
3
  import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
3
4
  import {
@@ -6,6 +7,7 @@ import {
6
7
  } from "../storage/policy-job";
7
8
  import { abortRestoreTrashJobAsync } from "../storage/restore-job";
8
9
  import { stopStorageCleanupScheduler } from "../storage/policy-scheduler";
10
+ import { stopLabAutomationScheduler, requestLabAutomationShutdown } from "../lab/automation/orchestrator";
9
11
  import { stopStateStoreSweeper } from "../lib/state-store-sweeper";
10
12
  import {
11
13
  cancelQueuedStorageWorkerSpawns,
@@ -433,12 +435,16 @@ export async function drainAndShutdown(
433
435
  console.warn("[cursor] background shell drain incomplete", shellResult.value);
434
436
  }
435
437
 
436
- // Debounced replay-state snapshot may still be pending; flush so the last completed turn's
437
- // previous_response_id chain survives the restart this shutdown is usually part of.
438
- const responseStateFlush = await Promise.allSettled([flushResponseState()]);
439
- if (responseStateFlush[0]?.status === "rejected") {
438
+ // Debounced replay-state snapshots may still be pending; flush so the last completed turn's
439
+ // previous_response_id chain and antigravity thought signatures survive the restart this
440
+ // shutdown is usually part of.
441
+ const stateFlush = await Promise.allSettled([flushResponseState(), flushAntigravityReplay()]);
442
+ if (stateFlush[0]?.status === "rejected") {
440
443
  console.warn("[responses] state flush during shutdown failed");
441
444
  }
445
+ if (stateFlush[1]?.status === "rejected") {
446
+ console.warn("[antigravity] replay flush during shutdown failed");
447
+ }
442
448
 
443
449
  // Tear down opt-in storage policy timers / worker / live-config sink so they cannot fire after stop.
444
450
  // Await worker thread exit: on Windows, a still-exiting Bun Worker under
@@ -446,6 +452,8 @@ export async function drainAndShutdown(
446
452
  // Abort each job independently so one wedged join cannot skip the other,
447
453
  // then drain leftovers; failures must not prevent `server.stop`.
448
454
  stopStorageCleanupScheduler();
455
+ requestLabAutomationShutdown();
456
+ stopLabAutomationScheduler();
449
457
  stopStateStoreSweeper();
450
458
  // The overlay reconciler is owner-scoped: the startServer stop override
451
459
  // releases THIS server's lease through runListenerShutdown →
@@ -0,0 +1,90 @@
1
+ import { readRuntimePort, type RuntimePortState } from "../config";
2
+ import { createLocalAttestationChallenge } from "../lib/local-management-attestation";
3
+ import {
4
+ LOCAL_MANAGEMENT_CAPABILITY_HEADER,
5
+ LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
6
+ LOCAL_MANAGEMENT_CAPABILITY_TTL_MS,
7
+ LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
8
+ LOCAL_MANAGEMENT_NONCE_HEADER,
9
+ createLocalManagementReadCapability,
10
+ type LocalManagementReadPath,
11
+ } from "../lib/local-management-capability";
12
+ import { directLocalHttpFetch } from "./direct-local-http";
13
+ import { probeHostname, type LiveProxy } from "./proxy-liveness";
14
+
15
+ export type LocalManagementReadResult =
16
+ | { kind: "response"; response: Response; targetPid: number }
17
+ | { kind: "unavailable"; reason: "unattested-target" | "runtime-mismatch" | "capability-unavailable" | "transport" };
18
+
19
+ export interface LocalManagementReadDeps {
20
+ fetchImpl?: typeof fetch;
21
+ readRuntime?: (pid: number) => RuntimePortState | null;
22
+ createNonce?: () => string;
23
+ now?: () => number;
24
+ }
25
+
26
+ export interface LocalManagementReadRequestDeps extends LocalManagementReadDeps {
27
+ timeoutMs?: number;
28
+ }
29
+
30
+ /**
31
+ * Read one exact local management endpoint without sending a reusable admin credential.
32
+ *
33
+ * The runtime record is the protected source of the per-process key. The server proves
34
+ * it owns that key by accepting a single-use capability bound to this GET, path, PID,
35
+ * port, and short expiry.
36
+ */
37
+ export async function fetchBoundLocalManagementRead(
38
+ target: LiveProxy,
39
+ path: LocalManagementReadPath,
40
+ deps: LocalManagementReadRequestDeps = {},
41
+ ): Promise<LocalManagementReadResult> {
42
+ if (
43
+ target.source !== "runtime"
44
+ || target.pid === null
45
+ || !Number.isSafeInteger(target.pid)
46
+ || target.pid <= 0
47
+ ) {
48
+ return { kind: "unavailable", reason: "unattested-target" };
49
+ }
50
+ const readRuntime = deps.readRuntime ?? readRuntimePort;
51
+ const runtime = readRuntime(target.pid);
52
+ if (
53
+ !runtime?.attestationSecret
54
+ || runtime.pid !== target.pid
55
+ || runtime.port !== target.port
56
+ ) {
57
+ return { kind: "unavailable", reason: "runtime-mismatch" };
58
+ }
59
+
60
+ const nonce = (deps.createNonce ?? createLocalAttestationChallenge)();
61
+ const expiresAt = (deps.now ?? Date.now)() + LOCAL_MANAGEMENT_CAPABILITY_TTL_MS;
62
+ const capability = createLocalManagementReadCapability(
63
+ runtime.attestationSecret,
64
+ nonce,
65
+ "GET",
66
+ path,
67
+ target.pid,
68
+ target.port,
69
+ expiresAt,
70
+ );
71
+ if (!capability) return { kind: "unavailable", reason: "capability-unavailable" };
72
+
73
+ try {
74
+ const response = await (deps.fetchImpl ?? directLocalHttpFetch)(
75
+ `http://${probeHostname(target.hostname)}:${target.port}${path}`,
76
+ {
77
+ headers: {
78
+ [LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
79
+ [LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
80
+ [LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER]: String(expiresAt),
81
+ [LOCAL_MANAGEMENT_CAPABILITY_HEADER]: capability,
82
+ },
83
+ signal: AbortSignal.timeout(deps.timeoutMs ?? 4_000),
84
+ },
85
+ );
86
+ return { kind: "response", response, targetPid: target.pid };
87
+ } catch {
88
+ return { kind: "unavailable", reason: "transport" };
89
+ }
90
+ }