@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -21,13 +21,19 @@ import { LABEL, TASK, serviceLogPath } from "./state";
21
21
  * moved, nvm switch, reinstall) — the service manager would restart-loop on a dead path
22
22
  * while `schtasks`/`launchctl` still report "installed".
23
23
  */
24
- export function bakedServicePathsDiagnostic(): string | null {
24
+ export function bakedServicePathsDiagnostic(platform: NodeJS.Platform = process.platform): string | null {
25
25
  const state = readServiceInstallState();
26
26
  // A launcher install runs the launcher, not the baked pair, so the pair's existence says
27
27
  // nothing about whether the service can start. Judging the recorded launcher is both
28
28
  // necessary (a deleted launcher IS stale) and sufficient (a replaced version directory
29
29
  // is not, which is exactly what #2898 made routine).
30
30
  if (state?.launcherPath) {
31
+ // launchd no longer bakes a launcher at all: a recorded one can only have come from an
32
+ // install that handed the service token and proxy environment to a mutable PATH shim,
33
+ // so it is stale whether or not the file it names still exists.
34
+ if (platform === "darwin") {
35
+ return "STALE mutable launchd launcher — run 'ocx service repair' to re-bake trusted package paths";
36
+ }
31
37
  if (existsSync(state.launcherPath)) return null;
32
38
  return `STALE baked paths (missing: ${state.launcherPath}) — run 'ocx service repair' to re-bake`;
33
39
  }
@@ -12,7 +12,7 @@ import { serviceApiTokenFilePath } from "../lib/service-secrets";
12
12
  import { recordOwnedConfigPath } from "../lib/config-ownership";
13
13
  import { writeServiceApiTokenFile, assertLiveServiceManagerAllowed } from "./guards";
14
14
  import { resolveServiceListenPort, buildServiceShellCommand, buildServiceLauncherShellCommand, installedServiceListenPort, resolvedProxyEnv } from "./health";
15
- import { SERVICE_MANAGED_ENV, LABEL, cliEntry, stableLauncherEntry, logPath, serviceStatePath, currentCodexSqliteHomeAbsolute, type ServiceInstallState, writeServiceInstallState, readServiceInstallState } from "./state";
15
+ import { SERVICE_MANAGED_ENV, LABEL, cliEntry, logPath, serviceStatePath, currentCodexSqliteHomeAbsolute, type ServiceInstallState, writeServiceInstallState, readServiceInstallState } from "./state";
16
16
  import { writeServiceDefinitionFile } from "./windows-ops";
17
17
  import { readTextOrNull } from "./windows-taskxml";
18
18
 
@@ -26,20 +26,18 @@ function plistString(value: string): string {
26
26
  }
27
27
 
28
28
  /**
29
- * Render the launchd plist. Mirrors `buildUnit`: when `deps.launcher` names a stable `ocx`
30
- * executable, the job execs that launcher instead of the package-local Bun + CLI pair, so a
31
- * version-manager upgrade (mise, asdf, nvm) that replaces the package directory is picked up
32
- * on the next launchd start instead of leaving the old build serving (#3464 — the macOS
33
- * counterpart of #2898). Discovery belongs to `installLaunchd()`; the default here is the
34
- * legacy pair so callers and tests stay hermetic.
29
+ * Render the launchd plist from the package-local Bun + CLI pair. Unlike systemd, launchd
30
+ * must not hand the service token and proxy environment to a PATH-discovered launcher: a
31
+ * mutable version-manager shim could be replaced after installation and run with those
32
+ * secrets. The legacy `launcher` dependency remains accepted for API compatibility but is
33
+ * ignored.
35
34
  */
36
35
  export function buildPlist(
37
36
  proxyEnv: { name: string; value: string }[] = resolvedProxyEnv(),
38
- deps: { launcher?: string | null; runtime?: DurableBunRuntime } = {},
37
+ deps: { runtime?: DurableBunRuntime; launcher?: string | null } = {},
39
38
  ): string {
40
39
  const runtime = deps.runtime ?? durableBunRuntime();
41
40
  const { bun, bunRuntimeSource, cli } = cliEntry(runtime);
42
- const launcher = deps.launcher ?? null;
43
41
  const log = logPath();
44
42
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
45
43
  const codexHome = process.env.CODEX_HOME?.trim();
@@ -52,16 +50,8 @@ export function buildPlist(
52
50
  // (src/cli/index.ts preserveRouting). Only the wrapper writes this second marker, so
53
51
  // the dashboard-stop refusal below can tell a real launchd job from an ordinary child.
54
52
  ` <key>${SERVICE_MANAGED_ENV}</key><string>1</string>`,
55
- ...(launcher ? [] : [
56
- ` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
57
- ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
58
- ]),
59
- // A launcher resolves the current package's bundled Bun after every upgrade. Preserve
60
- // only a proof-bound shell override; baking a package-local path here would recreate
61
- // the version-manager pin that launcher mode exists to remove (same rule as buildUnit).
62
- launcher && runtime.source === "override"
63
- ? ` <key>${runtime.overrideEnv}</key><string>${plistString(runtime.path)}</string>`
64
- : null,
53
+ ` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
54
+ ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
65
55
  ` <key>PATH</key><string>${plistString(path)}</string>`,
66
56
  codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
67
57
  codexSqliteHome ? ` <key>CODEX_SQLITE_HOME</key><string>${plistString(codexSqliteHome)}</string>` : null,
@@ -69,7 +59,7 @@ export function buildPlist(
69
59
  ...proxyEnv.map(({ name, value }) =>
70
60
  ` <key>${name}</key><string>${plistString(value)}</string>`),
71
61
  ].filter((line): line is string => Boolean(line)).join("\n");
72
- const command = launchdServiceCommand(launcher, runtime);
62
+ const command = launchdServiceCommand(runtime);
73
63
  return `<?xml version="1.0" encoding="UTF-8"?>
74
64
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
75
65
  <plist version="1.0">
@@ -135,18 +125,16 @@ export function reusePreviousPlistPathVariable(previous: string, rendered: strin
135
125
  }
136
126
 
137
127
  /**
138
- * The exec line {@link buildPlist} bakes, for the launcher and runtime a single install
139
- * already resolved. Shared so `installLaunchd` can verify the live job against the exact
140
- * string it just wrote instead of re-deriving it from install state that has not been
141
- * written yet (a fresh install has no state, so `expectedLaunchdCommand` would hand back
142
- * the Bun + CLI pair and call a correctly loaded launcher job stale).
128
+ * The exec line {@link buildPlist} bakes, for the runtime a single install already
129
+ * resolved. Shared so `installLaunchd` can verify the live job against the exact string it
130
+ * just wrote instead of re-deriving it from install state that has not been written yet
131
+ * (a fresh install has no state, so `expectedLaunchdCommand` falls back to the package
132
+ * pair — identical here, but only by accident of the fresh state).
143
133
  */
144
134
  function launchdServiceCommand(
145
- launcher: string | null,
146
135
  runtime: DurableBunRuntime = durableBunRuntime(),
147
136
  port: number = resolveServiceListenPort(),
148
137
  ): string {
149
- if (launcher) return buildServiceLauncherShellCommand(launcher, port);
150
138
  const { bun, cli } = cliEntry(runtime);
151
139
  return buildServiceShellCommand(bun, cli, port);
152
140
  }
@@ -496,13 +484,14 @@ export function installLaunchd(deps: {
496
484
  // The previous definition, kept for rollback. An eviction whose bootstrap fails used to
497
485
  // end with the new plist on disk, nothing in launchd, and nothing listening.
498
486
  const previousPlist = wasInstalled ? readTextOrNull(p) : null;
499
- // Resolve the launcher ONCE and hand the same value to the plist and to install state,
500
- // so the staleness diagnostic judges exactly what launchd runs.
501
- const launcher = stableLauncherEntry();
487
+ // Keep launchd bound to the package paths selected by this trusted invocation. A PATH
488
+ // launcher can be replaced later and would inherit the service token and proxy
489
+ // environment.
502
490
  // The command THIS install bakes, not the one install state remembers: on a fresh
503
491
  // install there is no state yet, and after a lost state file `expectedLaunchdCommand`
504
- // falls back to the Bun + CLI pair and would call a correct launcher job stale (#3464).
505
- const expectedCommand = launchdServiceCommand(launcher);
492
+ // falls back to the Bun + CLI pair, which happens to be the same answer only by accident
493
+ // of the empty state.
494
+ const expectedCommand = launchdServiceCommand();
506
495
  const uid = process.getuid?.() ?? 0;
507
496
  const guiDomain = launchdGuiDomain();
508
497
  const guiTarget = `${guiDomain}/${LABEL}`;
@@ -523,7 +512,7 @@ export function installLaunchd(deps: {
523
512
  );
524
513
  }
525
514
 
526
- let rendered = buildPlist(resolvedProxyEnv(), { launcher });
515
+ let rendered = buildPlist(resolvedProxyEnv());
527
516
  if (previousPlist !== null && previousPlist !== rendered && verdict.state === "loaded-current") {
528
517
  // The live job runs exactly the exec line this install baked, so the definition on disk
529
518
  // IS the one launchd is running: keep the PATH it already carries instead of replacing
@@ -553,7 +542,7 @@ export function installLaunchd(deps: {
553
542
  try { chmodSync(p, 0o600); } catch { /* best-effort */ }
554
543
  // Install state is refreshed because it is what `expectedLaunchdCommand` reads, and
555
544
  // a repair that leaves it stale re-creates the false "OLDER plist" report.
556
- writeServiceInstallState("scheduler", launcher);
545
+ writeServiceInstallState("scheduler");
557
546
  console.log("ℹ️ service is already loaded from the current plist; nothing to do.");
558
547
  // The ONLY `reloaded: false` exit: the process launchd was running when this command
559
548
  // started is still running, same pid. `ocx service restart` turns that into a kickstart.
@@ -652,7 +641,7 @@ export function installLaunchd(deps: {
652
641
  `⚠️ launchctl accepted the bootstrap but the job state could not be verified — ${
653
642
  verdict.detail ?? "launchctl could not be asked"}. Check: launchctl print ${guiTarget}`,
654
643
  );
655
- writeServiceInstallState("scheduler", launcher);
644
+ writeServiceInstallState("scheduler");
656
645
  return { reloaded: true };
657
646
  }
658
647
 
@@ -691,7 +680,7 @@ export function installLaunchd(deps: {
691
680
  + `then re-run '${wasInstalled ? "ocx service repair" : "ocx service install"}'.`,
692
681
  );
693
682
  }
694
- writeServiceInstallState("scheduler", launcher);
683
+ writeServiceInstallState("scheduler");
695
684
  // The rollback copy has done its job: the new definition is verified loaded. Leaving it
696
685
  // behind makes the NEXT repair's backup ambiguous (which failure did it come from?) and
697
686
  // `uninstall` the only thing that ever cleaned it up.
@@ -730,7 +719,7 @@ export function restartLaunchdJob(deps: {
730
719
  const run = deps.launchctl ?? runLaunchctl;
731
720
  const target = `${launchdGuiDomain()}/${LABEL}`;
732
721
  const expectedCommand = deps.expectedCommand
733
- ?? (() => launchdServiceCommand(stableLauncherEntry()));
722
+ ?? (() => launchdServiceCommand());
734
723
  const kicked = run(["kickstart", "-k", target]);
735
724
  const verdict = (deps.probe ?? probeLaunchdLoadState)({ expectedCommand });
736
725
  if (!kicked.ok || verdict.state === "not-loaded" || verdict.state === "loaded-stale") {
@@ -12,7 +12,7 @@ import { dirname, join } from "node:path";
12
12
  import { getConfigDir } from "../config";
13
13
  import { OCX_ELEVATED_STAGING_UNREADABLE, runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError, type StagedWindowsTaskXml } from "../lib/windows-elevation";
14
14
  import { defaultWinswEntry, installWinswService, statusWinswRaw, uninstallWinswService, WINSW_SERVICE_ID, type WinswStatus } from "../lib/winsw";
15
- import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenSecretDir } from "../lib/windows-secret-acl";
15
+ import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenElevatedStageDir, hardenElevatedStagePath, hardenSecretDir } from "../lib/windows-secret-acl";
16
16
  import { recordOwnedConfigPath } from "../lib/config-ownership";
17
17
  import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
18
18
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
@@ -192,19 +192,24 @@ export interface ElevatedSchedulerStagingDeps {
192
192
  * alone:
193
193
  *
194
194
  * - **Access.** The directory is created fresh by `mkdtemp`, then ACL-hardened before
195
- * anything is written into it, so another local account cannot read or replace the
196
- * payload while the UAC prompt is open. Hardening the directory first is what makes
197
- * the file private from the moment it exists.
195
+ * anything is written into it, so no non-administrator account can read or replace
196
+ * the payload while the UAC prompt is open. Administrators get read — never write —
197
+ * because an over-the-shoulder UAC prompt answered with a different administrator's
198
+ * credentials runs the elevated process as that admin (#4779); tamper-evidence does
199
+ * not depend on the DACL, it comes from the pinned handles and the digest below.
200
+ * Hardening the directory first is what makes the file private from the moment it
201
+ * exists.
198
202
  * - **No reparse point.** Each artifact is inspected with `lstat` and rejected unless it
199
203
  * is what it claims to be. `wx` already refuses to create over an existing name, which
200
204
  * is the atomic step here — there is no replace path to race, because every path is
201
205
  * inside a directory that did not exist a moment ago. The explicit check is what keeps
202
206
  * that guarantee from depending on a reading of `O_EXCL` semantics.
203
- * - **Tamper evidence.** The digest is taken over the exact bytes written, and the
204
- * elevated script recomputes it over the bytes it reads. An ACL cannot cover this:
205
- * a process running as the same user has the same SID and can rewrite the file, so
206
- * the digest is the only thing that makes such a swap fail closed rather than
207
- * silently register a different task definition.
207
+ * - **Pinned namespace and content.** Before UAC, the launcher opens every ancestor and
208
+ * payload without following reparse points. Ancestor handles share read/write but
209
+ * deny delete, so the namespace cannot be redirected while sibling payloads can
210
+ * still be written; payload handles deny write/delete outright. Those handles stay
211
+ * open until elevation exits. The digest then verifies the exact,
212
+ * length-bounded bytes read by the elevated process.
208
213
  *
209
214
  * Payloads are UTF-16LE with no BOM, and the elevated process decodes them straight into
210
215
  * `Register-ScheduledTask`. What is hashed is therefore exactly what is registered, with
@@ -217,11 +222,11 @@ export function stageElevatedSchedulerRegistration(
217
222
  ): StagedElevatedSchedulerRegistration {
218
223
  const createStageDir = deps.createStageDir
219
224
  ?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
220
- const hardenDir = deps.hardenDir ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
225
+ const hardenDir = deps.hardenDir ?? ((path: string) => { hardenElevatedStageDir(path, { required: true }); });
221
226
  const writePayload = deps.writePayload ?? ((path: string, bytes: Buffer) => {
222
227
  writeFileSync(path, bytes, { flag: "wx", mode: 0o600 });
223
228
  });
224
- const hardenPath = deps.hardenPath ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
229
+ const hardenPath = deps.hardenPath ?? ((path: string) => { hardenElevatedStagePath(path, { required: true }); });
225
230
  const inspect = deps.inspect ?? ((path: string) => lstatSync(path));
226
231
  const removeStageDir = deps.removeStageDir ?? ((path: string) => { rmdirSync(path); });
227
232
 
@@ -266,7 +271,7 @@ export function stageElevatedSchedulerRegistration(
266
271
  throw new Error(`Refusing to stage an elevated Task Scheduler payload through a redirected path: ${path}`);
267
272
  }
268
273
  hardenPath(path);
269
- return { path, sha256: createHash("sha256").update(bytes).digest("hex") };
274
+ return { path, byteLength: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
270
275
  };
271
276
  return {
272
277
  xml: stage("register.xml", xml),
@@ -293,10 +298,9 @@ export function stageElevatedSchedulerRegistration(
293
298
  *
294
299
  * The elevated process runs hidden, so nothing it writes survives; only the exit code
295
300
  * crosses back. That makes an unexplained code the whole user-facing error, which is
296
- * exactly what made the ENAMETOOLONG in #4692 expensive to diagnose. Staging introduces
297
- * one new failure of its own — the payload is readable only by the account that created
298
- * it, so an elevation answered with a different administrator's credentials cannot open
299
- * it — and that one gets named along with its remedy rather than surfacing as a number.
301
+ * exactly what made the ENAMETOOLONG in #4692 expensive to diagnose. Staging has one
302
+ * failure of its own — the elevated process cannot open the payload — and that one
303
+ * gets named along with its remedy rather than surfacing as a number.
300
304
  */
301
305
  export function describeElevatedRegistrationFailure(
302
306
  failureLabel: string,
@@ -305,10 +309,10 @@ export function describeElevatedRegistrationFailure(
305
309
  ): string {
306
310
  if (exitCode === OCX_ELEVATED_STAGING_UNREADABLE) {
307
311
  return `${failureLabel}: the elevated process could not read the staged task definition in `
308
- + `${stageDir}. That directory is readable only by the account that staged it, so this `
309
- + "happens when the UAC prompt was answered with a different administrator account. "
310
- + "Approve the prompt as the signed-in user, or run the command again from a session "
311
- + "already elevated as that user.";
312
+ + `${stageDir}. That directory grants read access to the staging account and to `
313
+ + "administrators, so the hardening did not take effect there or the file was "
314
+ + "replaced. Retry the command; if it keeps failing, run it again from a session "
315
+ + "already elevated as an administrator.";
312
316
  }
313
317
  return `${failureLabel} with exit code ${exitCode}.`;
314
318
  }
@@ -1,6 +1,37 @@
1
1
  import type { OcxProviderConfig } from "./provider";
2
2
  import type { CodexAccount } from "./accounts";
3
3
 
4
+ /** Public inference API exposure. Responses and Chat Completions are always served. */
5
+ export interface OcxApiSurfacesConfig {
6
+ /**
7
+ * `/v1/messages` and `/v1/messages/count_tokens`. Absent means "inherit
8
+ * `claudeCode.enabled !== false`"; a present non-boolean value disables the surface.
9
+ */
10
+ messages?: { enabled?: boolean };
11
+ }
12
+
13
+ /** Protocol delivery policy (devlog/_plan/260924_protocol_first_class). */
14
+ export interface OcxProtocolsConfig {
15
+ /**
16
+ * What happens when the final upstream wire cannot express a requested feature.
17
+ * `legacy` (default) keeps today's behavior; `reject` refuses before any upstream send.
18
+ */
19
+ unrepresentable?: "legacy" | "reject";
20
+ /** Staged rollout switches. Every switch defaults off and changes no semantics while off. */
21
+ rollout?: {
22
+ /** Eligible Chat candidates inside combos and policy routes send natively. */
23
+ nativeChatCombos?: boolean;
24
+ /** Proxy-managed key-auth Anthropic targets receive `/v1/messages` natively. */
25
+ managedMessagesNative?: boolean;
26
+ /** Extends managed native Messages to Anthropic OAuth accounts. Requires the switch above. */
27
+ managedMessagesNativeOAuth?: boolean;
28
+ /** Chat and Messages clients are encoded directly from adapter events. */
29
+ directEncoders?: boolean;
30
+ /** Compare the dispatch plan with the observed path; never sends a second request. */
31
+ shadowPlan?: boolean;
32
+ };
33
+ }
34
+
4
35
  /**
5
36
  * Claude Code inbound settings (devlog/260711_claude_inbound). Consumed by the
6
37
  * /v1/messages surface, the `ocx claude` launcher, and the GUI Claude page.
@@ -397,10 +428,17 @@ export interface OcxPrivacyConfig {
397
428
  maskEmails?: boolean;
398
429
  }
399
430
 
431
+ export interface OcxLinkTransportConfig {
432
+ tunnelPort: number;
433
+ linkId: string;
434
+ }
435
+
400
436
  export interface OcxClientConnectionConfig {
401
437
  serverUrl: string;
402
438
  managementUrl: string;
403
439
  managementTransport: "direct" | "relay";
440
+ transport?: "hub" | "link";
441
+ link?: OcxLinkTransportConfig;
404
442
  selectedClients: OcxConnectedClientId[];
405
443
  tokenEnv: "OPENCODEX_API_AUTH_TOKEN";
406
444
  apiKeyId: string;
@@ -527,6 +565,14 @@ export interface OcxConfig {
527
565
  googleAntigravityStaticCatalogVersion?: 1 | 2;
528
566
  /** Claude Code inbound + launcher settings. */
529
567
  claudeCode?: OcxClaudeCodeConfig;
568
+ /**
569
+ * Which public inference APIs this proxy serves. Read only through
570
+ * `resolveApiSurfaceSettings` in `src/protocols/settings.ts`, which fails closed on a
571
+ * malformed value and inherits `claudeCode.enabled` while no explicit value exists.
572
+ */
573
+ apiSurfaces?: OcxApiSurfacesConfig;
574
+ /** Protocol delivery policy and rollout switches; see `resolveProtocolSettings`. */
575
+ protocols?: OcxProtocolsConfig;
530
576
  /**
531
577
  * Per-client durable intent. This phase owns only `codex`; later phases extend
532
578
  * one key at a time rather than widening a shared union.
@@ -742,6 +788,12 @@ export interface OcxConfig {
742
788
  timeoutMs?: number;
743
789
  /** Maximum in-memory ciphertext-to-assignment entries. Default: 200. */
744
790
  cacheEntries?: number;
791
+ /**
792
+ * Extra recovery sends when ChatGPT rejects with a transient 5xx or the transport
793
+ * fails, sharing the same credential, deadline, and cache flight (#3661).
794
+ * Default: 0 (single attempt); maximum: 2.
795
+ */
796
+ retries?: number;
745
797
  };
746
798
  /**
747
799
  * Quota-reset detection and notification. Absent means off: no detection, no timer, no sink.
@@ -1136,7 +1188,7 @@ export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-fir
1136
1188
 
1137
1189
  export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilization";
1138
1190
 
1139
- export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
1191
+ export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window" | "jev";
1140
1192
  export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
1141
1193
  export type OcxComboDefaultEffortMode = "fallback" | "force";
1142
1194
 
@@ -1161,6 +1213,11 @@ export interface OcxComboTarget {
1161
1213
  model: string;
1162
1214
  /** Relative target weight for round-robin batches and random selection. Default 1; valid range 1..10000. */
1163
1215
  weight?: number;
1216
+ /**
1217
+ * Exact efforts JEV may choose for this target. Omit to allow every effort the
1218
+ * target currently advertises; an explicit list must be non-empty.
1219
+ */
1220
+ reasoningEfforts?: OcxComboDefaultEffort[];
1164
1221
  /**
1165
1222
  * Marks an emergency-only target. Inert unless the combo sets
1166
1223
  * `cooldownWaitPolicy`, and never makes a target permanently ineligible —
package/src/types.ts CHANGED
@@ -61,6 +61,8 @@ export type {
61
61
  } from "./types/request";
62
62
 
63
63
  export type {
64
+ OcxApiSurfacesConfig,
65
+ OcxProtocolsConfig,
64
66
  OcxClaudeCodeConfig,
65
67
  OcxClaudeDesktopFamily,
66
68
  OcxClaudeDesktopAssignment,
@@ -20,7 +20,6 @@ export function sweepUpdateLeftovers(args: {
20
20
  log?: (line: string) => void;
21
21
  deps?: UpdateFsDeps;
22
22
  }): {
23
- removed: string[];
24
23
  inUse: Array<{ path: string; code: string }>;
25
24
  recent: string[];
26
25
  notOwned: string[];
@@ -17,9 +17,9 @@
17
17
  * <scopeDir>/.ocx-recovery.json double-fault marker with a one-line restore
18
18
  *
19
19
  * A staging directory is created exclusively and immediately carries an ownership marker
20
- * (`.ocx-update-owner.json`). Only a marked staging directory is ever swept by a later
21
- * update; anything else next to the package — an unmarked stage from an older updater, npm's
22
- * own `.<name>-<random>` rename-aside, a link — is reported and left alone (#5624).
20
+ * (`.ocx-update-owner.json`). Later updates report leftovers but do not delete them: a marker
21
+ * proves provenance only while this process owns the fresh path, not after another local
22
+ * process could have replaced it. Other neighbouring entries are likewise left alone (#5624).
23
23
  */
24
24
  import { spawnSync } from "node:child_process";
25
25
  import { randomBytes } from "node:crypto";
@@ -169,9 +169,8 @@ function stampedName(prefix) {
169
169
  export const UPDATE_OWNER_MARKER = ".ocx-update-owner.json";
170
170
  const STAGE_PREFIX = ".ocx-staging-";
171
171
  /**
172
- * A marked stage younger than this is left alone by the sweep. npm's staging install is bounded
173
- * at three minutes by the launcher; the floor is far above it so an update running from another
174
- * home against the same global prefix never loses its in-flight stage.
172
+ * A marked stage younger than this is reported as recent. npm's staging install is bounded at
173
+ * three minutes by the launcher; the floor distinguishes likely in-flight work from leftovers.
175
174
  */
176
175
  export const STALE_STAGE_MIN_AGE_MS = 30 * 60 * 1000;
177
176
  const RETRYABLE_REMOVE_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
@@ -256,14 +255,14 @@ export function removeOwnedStage(stageRoot, deps = {}) {
256
255
  }
257
256
 
258
257
  /**
259
- * Clear what earlier update attempts left next to the package, without ever deleting anything
260
- * this updater cannot prove it created. Never throws and never fails the update: every stage is
261
- * a fresh, uniquely named directory, so a leftover can only cost disk space, not block staging.
258
+ * Report what earlier update attempts left next to the package. A marker is forgeable and a
259
+ * pathname can be replaced after inspection, so no later process may recursively delete a
260
+ * leftover by that pathname. Never throws or fails the update: fresh unique stages step around it.
262
261
  */
263
262
  export function sweepUpdateLeftovers({ packageDir, pkgName, log = () => {}, deps = {} }) {
264
263
  const scopeDir = dirname(packageDir);
265
264
  const now = (deps.now ?? Date.now)();
266
- const result = { removed: [], inUse: [], recent: [], notOwned: [] };
265
+ const result = { inUse: [], recent: [], notOwned: [] };
267
266
  let names = [];
268
267
  try {
269
268
  names = readdirSync(scopeDir);
@@ -281,9 +280,7 @@ export function sweepUpdateLeftovers({ packageDir, pkgName, log = () => {}, deps
281
280
  } else if (now - marker.createdAt < STALE_STAGE_MIN_AGE_MS) {
282
281
  result.recent.push(full);
283
282
  } else {
284
- const removal = removeOwnedStage(full, deps);
285
- if (removal.removed) result.removed.push(full);
286
- else result.inUse.push({ path: full, code: removal.code });
283
+ result.inUse.push({ path: full, code: "ESTALE" });
287
284
  }
288
285
  } else if (name.startsWith(renameAsidePrefix)) {
289
286
  result.notOwned.push(full);
@@ -291,9 +288,10 @@ export function sweepUpdateLeftovers({ packageDir, pkgName, log = () => {}, deps
291
288
  }
292
289
  // Names only: the full path under a user-scoped npm prefix carries the account name, and
293
290
  // this logger is the launcher's console.
294
- for (const path of result.removed) log("Removed a staging directory left by an earlier update: " + basename(path));
295
291
  for (const entry of result.inUse) {
296
- log("Left an earlier update's staging directory in place (" + entry.code + "; a file inside is still in use); the next update retries it: " + basename(entry.path));
292
+ log(entry.code === "ESTALE"
293
+ ? "Left an earlier update's staging directory in place; delete it by hand once no OpenCodex process is running from it: " + basename(entry.path)
294
+ : "Left an earlier update's staging directory in place (could not remove it: " + entry.code + "): " + basename(entry.path));
297
295
  }
298
296
  for (const path of result.notOwned) {
299
297
  log("Not removing " + basename(path) + " next to the package: this updater did not create it. Delete it by hand once no OpenCodex process is running from it.");
@@ -393,7 +391,7 @@ export function transactionalNpmUpdate({
393
391
  }) {
394
392
  const rename = deps.rename ?? renameSync;
395
393
  const scopeDir = dirname(packageDir);
396
- // Leftovers from earlier attempts are cleared or stepped around first; this never fails.
394
+ // Leftovers from earlier attempts are only reported; fresh unique stages step around them. This never fails.
397
395
  try { sweepUpdateLeftovers({ packageDir, pkgName, log, deps }); } catch { /* report-only */ }
398
396
  let stageRoot;
399
397
  // GLOBAL-style staging (-g --prefix): npm nests the package's dependencies INSIDE the
@@ -419,7 +417,9 @@ export function transactionalNpmUpdate({
419
417
  const discardStage = () => {
420
418
  const removal = removeOwnedStage(stageRoot, deps);
421
419
  if (!removal.removed) {
422
- log("Left this update's staging directory in place (" + removal.code + "; a file inside is still in use); the next update removes it: " + basename(stageRoot));
420
+ // EACCES/EUNKNOWN do not prove an open file, so the message names the failure
421
+ // without blaming a process; the code stays for whoever reads the log.
422
+ log("Left this update's staging directory in place (could not remove it: " + removal.code + "); later updates will not remove it either — delete it by hand once no OpenCodex process is running from it: " + basename(stageRoot));
423
423
  }
424
424
  };
425
425
  const spec = pkgName + "@" + (targetVersion || tag);