@bitkyc08/opencodex 2.10.2 → 2.11.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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -17,6 +17,8 @@ export interface HealthzIdentity {
17
17
  version?: unknown;
18
18
  uptime?: unknown;
19
19
  pid?: unknown;
20
+ port?: unknown;
21
+ restartCapability?: unknown;
20
22
  }
21
23
 
22
24
  export interface LivenessIo {
@@ -47,6 +49,9 @@ export interface LivenessIo {
47
49
  nowFn?: () => number;
48
50
  }
49
51
 
52
+ /** Default per-probe fetch ceiling shared by liveness and readiness probes. */
53
+ export const DEFAULT_PROBE_TIMEOUT_MS = 750;
54
+
50
55
  /** Default probe options for service stop / orphan cleanup — a just-bound proxy can miss a single 750ms probe. */
51
56
  export const SERVICE_STOP_LIVENESS: Pick<LivenessIo, "timeoutMs" | "attempts"> = {
52
57
  timeoutMs: 1500,
@@ -95,7 +100,7 @@ export async function proxyIdentityAt(
95
100
  const fetchFn = io.fetchFn ?? fetch;
96
101
  const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
97
102
  const nowFn = io.nowFn ?? Date.now;
98
- const baseTimeoutMs = io.timeoutMs ?? 750;
103
+ const baseTimeoutMs = io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
99
104
  const requestedAttempts = Math.trunc(io.attempts ?? 1);
100
105
  const attempts = Number.isNaN(requestedAttempts)
101
106
  ? 1
@@ -138,6 +143,11 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
138
143
  const verifyPidFn = io.verifyPidFn ?? verifyPidIdentity;
139
144
  const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
140
145
  const configFn = io.configFn ?? loadConfig;
146
+ const nowFn = io.nowFn ?? Date.now;
147
+ const deadlineAt = io.deadlineAt;
148
+ const probeIo: LivenessIo = io;
149
+ const budgetExhausted = (): boolean =>
150
+ deadlineAt !== undefined && nowFn() >= deadlineAt;
141
151
 
142
152
  // The cheap pid is discovery-only. Before it can appear in a returned (killable) result
143
153
  // it must pass the full identity check AND the verifier must echo the exact candidate —
@@ -160,8 +170,9 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
160
170
  if (pid) {
161
171
  const runtime = readRuntimeFn(pid);
162
172
  if (runtime?.port) {
173
+ if (budgetExhausted()) return null;
163
174
  probedPort = runtime.port;
164
- const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, io);
175
+ const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, probeIo);
165
176
  if (identity) {
166
177
  // healthz confirmed the pid itself → trusted; a pidless legacy body did not,
167
178
  // so the cheap pid must pass full identity verification before it is returned.
@@ -176,8 +187,9 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
176
187
  // identity-probe it so ensure/update/stop see the live proxy instead of shadowing it.
177
188
  const record = readRuntimeFn();
178
189
  if (record?.port && record.port !== probedPort) {
190
+ if (budgetExhausted()) return null;
179
191
  const expectedPid = typeof record.pid === "number" ? record.pid : undefined;
180
- const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, io);
192
+ const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, probeIo);
181
193
  // Only the healthz-reported pid is authoritative here. The record's pid may be stale
182
194
  // (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
183
195
  // would hand destructive callers (stopProxy → kill fallback) a reusable pid.
@@ -188,7 +200,8 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
188
200
 
189
201
  const config = configFn();
190
202
  const port = config.port ?? 10100;
191
- const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
203
+ if (budgetExhausted()) return null;
204
+ const identity = await proxyIdentityAt(port, { hostname: config.hostname }, probeIo);
192
205
  if (identity) {
193
206
  return {
194
207
  pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
@@ -199,3 +212,115 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
199
212
  }
200
213
  return null;
201
214
  }
215
+
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+ // Readiness (/readyz) strict probe.
218
+ //
219
+ // Liveness (/healthz) confirms the process answers; readiness confirms the
220
+ // post-startup Codex sync has settled. A readiness probe is identity-checked the
221
+ // same way liveness is, AND additionally enforces the full /readyz contract so
222
+ // an adversarial or malformed body can never count as ready:
223
+ //
224
+ // - HTTP 200 is required for status="ready"; HTTP 503 is required for pending
225
+ // or failed. Any other HTTP/body-status pairing is an invalid contract.
226
+ // - body.service must be exactly "opencodex".
227
+ // - body.version must be a non-empty string.
228
+ // - body.uptime must be a finite nonnegative number.
229
+ // - body.pid must be a positive integer; when `expectedPid` is supplied it must
230
+ // match exactly.
231
+ // - body.port must be an integer in 1..65535 and equal the probed port.
232
+ // - body.status must be exactly one of pending|ready|failed.
233
+ //
234
+ // Any unreachable, foreign, legacy, malformed, mismatched, or self-inconsistent
235
+ // response returns `null` so callers can never treat an invalid identity/contract
236
+ // as ready.
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+
239
+ interface ReadyzBody {
240
+ service?: unknown;
241
+ version?: unknown;
242
+ uptime?: unknown;
243
+ pid?: unknown;
244
+ port?: unknown;
245
+ status?: unknown;
246
+ }
247
+
248
+ export interface ReadinessProbeResult {
249
+ /** True ONLY for a valid 200 + status="ready" body with a matching pid. */
250
+ ready: boolean;
251
+ /** Fixed sanitized status. A foreign/unreadable body yields a `null` RESULT, never a `null` status. */
252
+ status: "ready" | "pending" | "failed";
253
+ /** Positive integer pid from a valid body. */
254
+ pid: number;
255
+ /** Integer port from a valid body. */
256
+ port: number;
257
+ }
258
+
259
+ export interface ReadinessProbeIo {
260
+ fetchFn?: typeof fetch;
261
+ timeoutMs?: number;
262
+ }
263
+
264
+ const READYZ_STATUS_VALUES = new Set<"ready" | "pending" | "failed">(["ready", "pending", "failed"]);
265
+
266
+ /**
267
+ * Validate a parsed /readyz body against the strict contract. Returns the
268
+ * sanitized probe result, or `null` when the body is foreign, legacy,
269
+ * malformed, or fails the pid/port checks. Pure (no I/O) so it is fully
270
+ * deterministic and unit-testable.
271
+ */
272
+ export function validateReadyzBody(
273
+ body: unknown,
274
+ port: number,
275
+ opts: { expectedPid?: number } = {},
276
+ ): ReadinessProbeResult | null {
277
+ if (!body || typeof body !== "object") return null;
278
+ const b = body as ReadyzBody;
279
+ if (b.service !== "opencodex") return null;
280
+ if (typeof b.version !== "string" || b.version.length === 0) return null;
281
+ if (typeof b.uptime !== "number" || !Number.isFinite(b.uptime) || b.uptime < 0) return null;
282
+ if (typeof b.pid !== "number" || !Number.isInteger(b.pid) || b.pid <= 0) return null;
283
+ if (
284
+ typeof b.port !== "number"
285
+ || !Number.isInteger(b.port)
286
+ || b.port < 1
287
+ || b.port > 65535
288
+ || b.port !== port
289
+ ) return null;
290
+ if (typeof b.status !== "string" || !READYZ_STATUS_VALUES.has(b.status as "ready" | "pending" | "failed")) return null;
291
+ const status = b.status as "ready" | "pending" | "failed";
292
+ if (opts.expectedPid !== undefined && b.pid !== opts.expectedPid) return null;
293
+ return { ready: status === "ready", status, pid: b.pid, port: b.port };
294
+ }
295
+
296
+ /**
297
+ * Identity- and contract-checked /readyz probe. Returns `null` when the
298
+ * endpoint is unreachable or the body fails the strict contract (foreign 200,
299
+ * legacy health-only body, non-JSON, missing/malformed/mismatched fields,
300
+ * wrong port/pid, or an HTTP/body-status inconsistency). Returns
301
+ * `{ready:false, ...}` when the body is ours but pending or failed. Returns
302
+ * `{ready:true, ...}` ONLY for a valid 200 body with `status:"ready"` and (when
303
+ * requested) a matching pid.
304
+ */
305
+ export async function probeReadiness(
306
+ port: number,
307
+ opts: { hostname?: string; expectedPid?: number } = {},
308
+ io: ReadinessProbeIo = {},
309
+ ): Promise<ReadinessProbeResult | null> {
310
+ const fetchFn = io.fetchFn ?? fetch;
311
+ try {
312
+ const res = await fetchFn(`http://${probeHostname(opts.hostname)}:${port}/readyz`, {
313
+ signal: AbortSignal.timeout(io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS),
314
+ });
315
+ // Parse even on 503: /readyz returns JSON with a sanitized status while pending.
316
+ const body = (await res.json().catch(() => null)) as unknown;
317
+ const parsed = validateReadyzBody(body, port, opts);
318
+ if (!parsed) return null;
319
+ // HTTP/body-status consistency: ready requires 200; pending/failed require 503.
320
+ if (parsed.status === "ready" && res.status !== 200) return null;
321
+ if (parsed.status !== "ready" && res.status !== 503) return null;
322
+ return parsed;
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Per-server readiness gate for the opencodex proxy.
3
+ *
4
+ * `GET /healthz` answers "is the process alive and serving HTTP?" the instant the
5
+ * listener binds. Readiness is stricter: the proxy is "ready" only after the
6
+ * post-startup Codex catalog/config sync (`syncModelsToCodex`) has settled with
7
+ * `ok=true` and no catalog-sync warning. Until then the process is live (Codex can
8
+ * open a socket) but not ready (a request would race the sync or hit a stale
9
+ * catalog), so clients should back off.
10
+ *
11
+ * Design contract (per P1 review):
12
+ * - NO module-global mutable state. Each `startServer` invocation gets its own
13
+ * private gate via `createReadinessGate()`, captured by that listener's
14
+ * closure. Starting/failing a second server in the same process can never
15
+ * reset or mutate the first server's gate.
16
+ * - Only the fixed sanitized status enum `pending | ready | failed` is stored
17
+ * and exposed. There is no `changedAt`, no free-form failure reason, no sync
18
+ * message, no warning text, no catalog path, no provider output, and no
19
+ * account data — those are private diagnostic data and are never exposed by
20
+ * `/readyz`.
21
+ */
22
+
23
+ /** Sanitized readiness state. Exactly these three values, nothing else. */
24
+ export type ReadinessStatus = "pending" | "ready" | "failed";
25
+
26
+ /**
27
+ * Private per-server readiness controller. The status starts at `pending` and
28
+ * transitions at most once (to `ready` or `failed`) when the post-startup sync
29
+ * settles. The gate is owned by the listener closure that requested it.
30
+ */
31
+ export interface ReadinessGate {
32
+ /** Current sanitized status. */
33
+ getStatus(): ReadinessStatus;
34
+ /** Mark the proxy ready (post-startup sync settled cleanly). */
35
+ markReady(): void;
36
+ /** Mark the proxy failed. No reason is stored or exposed. */
37
+ markFailed(): void;
38
+ }
39
+
40
+ /**
41
+ * Create a fresh private gate for one `startServer` invocation. The returned
42
+ * gate is the only way to read or mutate this server's readiness.
43
+ */
44
+ export function createReadinessGate(): ReadinessGate {
45
+ let status: ReadinessStatus = "pending";
46
+ return {
47
+ getStatus: () => status,
48
+ markReady: () => {
49
+ if (status === "pending") status = "ready";
50
+ },
51
+ markFailed: () => {
52
+ if (status === "pending") status = "failed";
53
+ },
54
+ };
55
+ }
56
+
57
+ /** Minimal shape of the post-startup sync outcome the gate cares about. */
58
+ export interface SyncOutcomeLike {
59
+ ok?: boolean;
60
+ warning?: string;
61
+ /** #1046: whether the sync actually rewrote the on-disk catalog/cache. */
62
+ catalogWritten?: boolean;
63
+ cacheSynced?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Drive the gate from the post-startup sync. Awaits `syncFn`; the gate goes to
68
+ * `ready` ONLY on `ok=true` with no nonempty warning. A throw, `null`, `ok=false`,
69
+ * or a nonempty warning transitions to `failed`. Used directly by `handleStart`
70
+ * so the startup transition is unit-testable without spawning the proxy. Returns
71
+ * the raw sync outcome so a caller that also needs the #1046 write flags (did the
72
+ * sync actually write the catalog/cache?) can keep them without a second call.
73
+ */
74
+ export async function runStartupReadinessSync(
75
+ gate: ReadinessGate,
76
+ syncFn: () => Promise<SyncOutcomeLike | null>,
77
+ ): Promise<SyncOutcomeLike | null> {
78
+ let result: SyncOutcomeLike | null;
79
+ try {
80
+ result = await syncFn();
81
+ } catch {
82
+ gate.markFailed();
83
+ return null;
84
+ }
85
+ if (result === null) {
86
+ gate.markFailed();
87
+ return null;
88
+ }
89
+ if (result.ok !== true) {
90
+ gate.markFailed();
91
+ return result;
92
+ }
93
+ if (result.warning !== undefined && result.warning !== "") {
94
+ gate.markFailed();
95
+ return result;
96
+ }
97
+ gate.markReady();
98
+ return result;
99
+ }