@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-BqrsSrIR.js +0 -50
  259. package/gui/dist/assets/index-C6SJrh0N.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -0,0 +1,151 @@
1
+ /** Read-only manager-to-proxy binding for consent-bound desktop stops. */
2
+ import { execFileSync } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+ import { verifyPidIdentity } from "../config/process-state";
5
+ import { statusWinswRaw } from "../lib/winsw";
6
+ import { systemdProperty } from "../service-manager-probe";
7
+ import { expectedLaunchdCommand, launchdEvictionTargets, probeLaunchdLoadState, runLaunchctl } from "./launchd";
8
+ import { probeSystemdUnitInactive, unitPath } from "./systemd";
9
+ import { probeWindowsSchedulerTask } from "./windows-scheduler";
10
+ import { TASK } from "./state";
11
+
12
+ export type GuardedManagerTarget =
13
+ | { kind: "absent" }
14
+ | { kind: "bound"; pid: number; managerPid: number; backend: "launchd" | "systemd" }
15
+ | { kind: "unknown"; reason: string };
16
+
17
+ export type GuardedManagerStopped = "inactive" | "active" | "unknown";
18
+
19
+ export function managerOwnsApprovedPid(
20
+ managerPid: number,
21
+ approvedPid: number,
22
+ parentOf: (pid: number) => number | null,
23
+ ): boolean {
24
+ let current = approvedPid;
25
+ const seen = new Set<number>();
26
+ for (let depth = 0; depth < 16; depth += 1) {
27
+ if (current === managerPid) return true;
28
+ if (!Number.isSafeInteger(current) || current <= 0 || seen.has(current)) return false;
29
+ seen.add(current);
30
+ const parent = parentOf(current);
31
+ if (parent === null || !Number.isSafeInteger(parent) || parent <= 0) return false;
32
+ current = parent;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ function parentPid(pid: number): number | null {
38
+ try {
39
+ const raw = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
40
+ encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "pipe"],
41
+ }).trim();
42
+ const value = Number(raw);
43
+ return /^\d+$/.test(raw) && Number.isSafeInteger(value) && value > 0 ? value : null;
44
+ } catch { return null; }
45
+ }
46
+
47
+ function positivePid(raw: string | null | undefined): number | null {
48
+ if (!raw || !/^\d+$/.test(raw)) return null;
49
+ const pid = Number(raw);
50
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
51
+ }
52
+
53
+ export interface GuardedManagerDeps {
54
+ platform?: NodeJS.Platform;
55
+ launchctl?: typeof runLaunchctl;
56
+ expectedCommand?: (port: number) => string;
57
+ systemdShow?: () => string;
58
+ parentOf?: (pid: number) => number | null;
59
+ verifyPid?: (pid: number) => number | null;
60
+ scheduler?: typeof probeWindowsSchedulerTask;
61
+ winsw?: typeof statusWinswRaw;
62
+ }
63
+
64
+ /** Only one current manager with a verified parent chain may be stopped by name. */
65
+ export function inspectGuardedManagerTarget(
66
+ approvedPid: number,
67
+ approvedPort: number,
68
+ deps: GuardedManagerDeps = {},
69
+ ): GuardedManagerTarget {
70
+ const platform = deps.platform ?? process.platform;
71
+ if ((deps.verifyPid ?? verifyPidIdentity)(approvedPid) !== approvedPid) {
72
+ return { kind: "unknown", reason: "approved process identity could not be verified" };
73
+ }
74
+ const parentOf = deps.parentOf ?? parentPid;
75
+ if (platform === "darwin") {
76
+ const run = deps.launchctl ?? runLaunchctl;
77
+ let managerPid: number | null = null;
78
+ try {
79
+ const expected = (deps.expectedCommand ?? expectedLaunchdCommand)(approvedPort);
80
+ for (const target of launchdEvictionTargets()) {
81
+ const result = run(["print", target]);
82
+ if (result.status === 112 || result.status === 113) continue;
83
+ if (result.status !== 0) return { kind: "unknown", reason: "launchd state is unreadable" };
84
+ if (managerPid !== null) return { kind: "unknown", reason: "multiple launchd jobs are loaded" };
85
+ const output = `${result.stdout}\n${result.stderr}`;
86
+ if (!output.includes(expected)) return { kind: "unknown", reason: "launchd command differs from registration" };
87
+ const pids = [...output.matchAll(/^\s*pid\s*=\s*(\d+)\s*$/gm)];
88
+ if (pids.length !== 1) return { kind: "unknown", reason: "launchd PID is ambiguous" };
89
+ managerPid = positivePid(pids[0]?.[1]);
90
+ if (managerPid === null) return { kind: "unknown", reason: "launchd PID is invalid" };
91
+ }
92
+ } catch { return { kind: "unknown", reason: "launchd state could not be verified" }; }
93
+ if (managerPid === null) return { kind: "absent" };
94
+ return managerOwnsApprovedPid(managerPid, approvedPid, parentOf)
95
+ ? { kind: "bound", pid: approvedPid, managerPid, backend: "launchd" }
96
+ : { kind: "unknown", reason: "launchd job does not own the approved process" };
97
+ }
98
+ if (platform === "linux") {
99
+ try {
100
+ const output = (deps.systemdShow ?? (() => execFileSync("systemctl", ["--user", "show", "-p", "LoadState", "-p", "ActiveState", "-p", "MainPID", "-p", "FragmentPath", "-p", "NeedDaemonReload", TASK], {
101
+ encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "pipe"],
102
+ })))();
103
+ const loaded = systemdProperty(output, "LoadState");
104
+ const active = systemdProperty(output, "ActiveState");
105
+ const pid = positivePid(systemdProperty(output, "MainPID"));
106
+ if (loaded === "not-found" && active === "inactive") return { kind: "absent" };
107
+ if (loaded === "loaded" && active === "inactive" && systemdProperty(output, "MainPID") === "0") return { kind: "absent" };
108
+ if (loaded !== "loaded" || active !== "active" || pid === null
109
+ || systemdProperty(output, "NeedDaemonReload") !== "no"
110
+ || resolve(systemdProperty(output, "FragmentPath") ?? "") !== resolve(unitPath())) {
111
+ return { kind: "unknown", reason: "systemd unit state is not current and active" };
112
+ }
113
+ return managerOwnsApprovedPid(pid, approvedPid, parentOf)
114
+ ? { kind: "bound", pid: approvedPid, managerPid: pid, backend: "systemd" }
115
+ : { kind: "unknown", reason: "systemd unit does not own the approved process" };
116
+ } catch { return { kind: "unknown", reason: "systemd state could not be verified" }; }
117
+ }
118
+ if (platform === "win32") {
119
+ const scheduler = (deps.scheduler ?? probeWindowsSchedulerTask)();
120
+ const native = (deps.winsw ?? statusWinswRaw)();
121
+ return scheduler.status === "absent" && native === "nonexistent"
122
+ ? { kind: "absent" }
123
+ : { kind: "unknown", reason: "Windows manager child PID cannot be proven" };
124
+ }
125
+ return { kind: "unknown", reason: "unsupported service manager platform" };
126
+ }
127
+
128
+ /** A guarded success needs manager absence after settlement and once more at publication. */
129
+ export async function observeGuardedManagerStopped(
130
+ manager: Exclude<GuardedManagerTarget, { kind: "unknown" }>,
131
+ deps: Pick<GuardedManagerDeps, "platform" | "scheduler" | "winsw"> & {
132
+ launchd?: typeof probeLaunchdLoadState;
133
+ systemd?: typeof probeSystemdUnitInactive;
134
+ } = {},
135
+ ): Promise<GuardedManagerStopped> {
136
+ try {
137
+ const platform = deps.platform ?? process.platform;
138
+ if (platform === "darwin") {
139
+ const state = (deps.launchd ?? probeLaunchdLoadState)().state;
140
+ return state === "not-loaded" ? "inactive" : state === "unknown" ? "unknown" : "active";
141
+ }
142
+ if (platform === "linux") return (deps.systemd ?? probeSystemdUnitInactive)();
143
+ if (platform === "win32" && manager.kind === "absent") {
144
+ const scheduler = (deps.scheduler ?? probeWindowsSchedulerTask)();
145
+ const native = (deps.winsw ?? statusWinswRaw)();
146
+ if (scheduler.status === "absent" && native === "nonexistent") return "inactive";
147
+ return scheduler.status === "unknown" || native === "unknown" ? "unknown" : "active";
148
+ }
149
+ } catch { return "unknown"; }
150
+ return "unknown";
151
+ }
@@ -1,7 +1,8 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { getConfigDir, loadConfig } from "../config";
4
- import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
4
+ import { withConfigMutationLockSync } from "../config/mutation-lock";
5
+ import { hardenReusedServiceApiToken, readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
5
6
  import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
6
7
  import { randomBytes } from "node:crypto";
7
8
  import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
@@ -9,8 +10,10 @@ import { recordOwnedConfigPath } from "../lib/config-ownership";
9
10
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
10
11
  import { diagnoseService } from "./diagnostics";
11
12
  import type { ServiceDiagnostic } from "./diagnostics";
12
- import { currentCodexHome, currentOpenCodexHome, normalizePathForCompare, readServiceInstallState } from "./state";
13
+ import { currentCodexHome, currentOpenCodexHome, normalizePathForCompare, resolveServiceState, serviceCodexHomeMatchesInstall } from "./state";
13
14
  import { resolveCodexSqliteHome } from "../codex/paths";
15
+ import type { CodexHomeDeps } from "../codex/home";
16
+ import { isLoopbackHostname } from "../codex/loopback-target";
14
17
  import { win32 } from "node:path";
15
18
 
16
19
  /**
@@ -42,16 +45,17 @@ export function serviceEnvironmentOwnedHere(): boolean {
42
45
  }
43
46
  }
44
47
 
45
- export function assertServiceEnvironmentMatchesInstall(): void {
46
- const state = readServiceInstallState();
47
- if (!state) return;
48
- const actualCodexHome = currentCodexHome();
49
- const expected = normalizePathForCompare(state.codexHome);
50
- const actual = normalizePathForCompare(actualCodexHome);
51
- if (expected !== actual) {
48
+ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {}): void {
49
+ const resolution = resolveServiceState();
50
+ // Unreadable state keeps its existing interactive semantics (see assertNativeTeardownOwned);
51
+ // unattended writes use the tri-state inspector, which reports it as unknown.
52
+ if (resolution.kind !== "state") return;
53
+ const state = resolution.state;
54
+ const actualCodexHome = currentCodexHome(deps);
55
+ if (!serviceCodexHomeMatchesInstall(state.codexHome, deps)) {
52
56
  throw new ServiceOwnershipError(
53
57
  `Service was installed with CODEX_HOME=${state.codexHome}, but current CODEX_HOME=${actualCodexHome}. ` +
54
- "Run the service command from the same Codex home so native Codex restore updates the correct config.",
58
+ `Rerun with CODEX_HOME=${state.codexHome} so native Codex restore updates the recorded home.`,
55
59
  );
56
60
  }
57
61
  const expectedOpenCodexHome = normalizePathForCompare(state.opencodexHome);
@@ -73,11 +77,6 @@ export function assertServiceEnvironmentMatchesInstall(): void {
73
77
  }
74
78
  }
75
79
 
76
- function isLoopbackHostname(hostname: string | undefined): boolean {
77
- const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
78
- return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
79
- }
80
-
81
80
  /**
82
81
  * The `ocx` command a user should rerun for the service state they actually have.
83
82
  *
@@ -228,42 +227,42 @@ function persistServiceApiToken(token: string): string {
228
227
  * connected to a hub the same file holds that hub's issued client key, which must not be
229
228
  * overwritten by a local install.
230
229
  *
230
+ * Provisioning runs inside the cross-process config mutation lock: client-key rotation
231
+ * replaces `service-api-token` and records the new fingerprint under the same lock, so a
232
+ * reuse republish or a fresh write here can never interleave with a committed rotation and
233
+ * silently roll its bytes back.
234
+ *
231
235
  * The PATH is logged; the value never is, and never reaches argv, a unit file or a plist.
232
236
  */
233
237
  export function writeServiceApiTokenFile(): ProvisionedServiceApiToken | null {
234
- const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
235
- if (token) {
236
- // Last line of defence: every install/repair path funnels through here, so a
237
- // collision cannot reach disk regardless of which caller ran (#2696).
238
- assertNotAdminToken(token);
239
- const path = persistServiceApiToken(token);
240
- console.log(`🔐 Data-plane token taken from OPENCODEX_API_AUTH_TOKEN and stored at ${path} (owner-only).`);
241
- return { path, origin: "env" };
242
- }
243
- if (isLoopbackHostname(loadConfig().hostname)) return null;
244
- const existing = readServiceApiTokenState();
245
- if (existing.kind === "present") {
246
- // The collision check is NOT only for the env branch. A file that already holds the admin
247
- // token -- hand-pasted before #2696, or written by the very incident this unit closes --
248
- // was silently accepted here, so `ocx status` reported `present (file)` and the hub
249
- // crash-looped at boot with no command pointing at the cause.
250
- const path = serviceApiTokenFilePath();
251
- assertNotAdminToken(existing.token, process.env, "file");
252
- // `readServiceApiTokenState` accepts any bounded regular file, so a reused token may well
253
- // be group- or world-readable. Tighten it on the way through rather than claiming
254
- // "owner-only" about a mode nobody checked; best-effort, since a non-owner cannot chmod
255
- // and failing the install over it would be worse than the loose mode.
256
- try { chmodSync(path, 0o600); } catch { /* best-effort */ }
257
- if (process.platform === "win32") hardenSecretPath(path, { required: false });
258
- // No log line: repair/restart hit this on every run and an unconditional notice about a
259
- // credential file trains operators to ignore the one that matters.
260
- return { path, origin: "file" };
261
- }
262
- if (existing.kind === "unsafe") throw new Error(`${existing.reason}: ${serviceApiTokenFilePath()}`);
263
- const path = persistServiceApiToken(randomBytes(32).toString("hex"));
264
- console.log(`🔐 Provisioned an owner-only data-plane token at ${path}; nothing needs to be exported by hand.`);
265
- console.log(" Remote machines get their own per-client key — run 'ocx hub invite' instead of copying this file.");
266
- return { path, origin: "generated" };
238
+ return withConfigMutationLockSync(() => {
239
+ const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
240
+ if (token) {
241
+ // Last line of defence: every install/repair path funnels through here, so a
242
+ // collision cannot reach disk regardless of which caller ran (#2696).
243
+ assertNotAdminToken(token);
244
+ const path = persistServiceApiToken(token);
245
+ console.log(`🔐 Data-plane token taken from OPENCODEX_API_AUTH_TOKEN and stored at ${path} (owner-only).`);
246
+ return { path, origin: "env" };
247
+ }
248
+ if (isLoopbackHostname(loadConfig().hostname)) return null;
249
+ const existing = hardenReusedServiceApiToken(token => assertNotAdminToken(token, process.env, "file"));
250
+ if (existing.kind === "present") {
251
+ // The collision check is NOT only for the env branch. A file that already holds the admin
252
+ // token -- hand-pasted before #2696, or written by the very incident this unit closes --
253
+ // was silently accepted here, so `ocx status` reported `present (file)` and the hub
254
+ // crash-looped at boot with no command pointing at the cause.
255
+ const path = serviceApiTokenFilePath();
256
+ // No log line: repair/restart hit this on every run and an unconditional notice about a
257
+ // credential file trains operators to ignore the one that matters.
258
+ return { path, origin: "file" };
259
+ }
260
+ if (existing.kind === "unsafe") throw new Error(`${existing.reason}: ${serviceApiTokenFilePath()}`);
261
+ const path = persistServiceApiToken(randomBytes(32).toString("hex"));
262
+ console.log(`🔐 Provisioned an owner-only data-plane token at ${path}; nothing needs to be exported by hand.`);
263
+ console.log(" Remote machines get their own per-client key — run 'ocx hub invite' instead of copying this file.");
264
+ return { path, origin: "generated" };
265
+ });
267
266
  }
268
267
 
269
268
  export function sh(cmd: string): string {
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Observe the two CLIs that can manage this runtime: the one baked into the preserved
3
+ * service registration, and the `ocx` the PATH resolves today.
4
+ *
5
+ * Both answers feed `assessServiceTakeoverCompatibility`, which refuses a takeover when
6
+ * either observation is unknown — a managing binary that cannot be named cannot be held to
7
+ * the ownership-aware floor. `absent` is a trustworthy answer; `unknown` is not.
8
+ *
9
+ * The PATH lookup is an in-module directory scan rather than `where`/`which`: spawning a
10
+ * shell builtin to find a binary makes the answer depend on the host's PATH handling, and
11
+ * this module exists so the resolve/claim path is deterministic and testable. And the one
12
+ * thing it must never do is spawn `process.execPath`: that binary is THIS CLI, and asking
13
+ * it for its version under `ocx --version` semantics is the recursion #5418 closed. When
14
+ * the located file IS this binary, its version is already known.
15
+ */
16
+ import { existsSync, statSync } from "node:fs";
17
+ import { spawnSync, type SpawnSyncReturns } from "node:child_process";
18
+ import { delimiter, posix, win32 } from "node:path";
19
+ import { parseStrictSemver } from "../lib/strict-semver";
20
+ import { packageVersion } from "../lib/package-version";
21
+ import {
22
+ registeredManagingCliInvocation,
23
+ type ManagingCliObservation,
24
+ type ManagingCliRole,
25
+ } from "./ownership-compatibility";
26
+ import type { ServiceInstallState } from "./state";
27
+
28
+ const VERSION_PROBE_TIMEOUT_MS = 5000;
29
+
30
+ export interface ManagingCliDeps {
31
+ /** Environment to scan PATH/PATHEXT in. Defaults to `process.env`. */
32
+ env?: Record<string, string | undefined>;
33
+ /** Spawn seam. Defaults to `spawnSync`. */
34
+ spawn?: typeof spawnSync;
35
+ /** The running binary, for the never-spawn-yourself rule. Defaults to `process.execPath`. */
36
+ execPath?: string;
37
+ /** This binary's own version, for the self-observation shortcut. */
38
+ ownVersion?: () => string;
39
+ /** Filesystem existence seam. */
40
+ exists?: (path: string) => boolean;
41
+ /** Validate the selected candidate as a regular file. */
42
+ isFile?: (path: string) => boolean;
43
+ /** Host platform override for tests. */
44
+ platform?: NodeJS.Platform;
45
+ }
46
+
47
+ /** The first strict-semver token in a `--version` line (`opencodex 2.61.0` → `2.61.0`). */
48
+ function versionFromOutput(stdout: string): string | null {
49
+ for (const token of stdout.trim().split(/\s+/)) {
50
+ if (parseStrictSemver(token)) return token;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function probeVersion(
56
+ executable: string,
57
+ args: readonly string[],
58
+ deps: Required<Pick<ManagingCliDeps, "spawn" | "platform" | "env">>,
59
+ ): ManagingCliObservation {
60
+ const identity = [executable, ...args].join(" ");
61
+ let result: SpawnSyncReturns<string>;
62
+ try {
63
+ const windowsShim =
64
+ deps.platform === "win32" && /\.(cmd|bat)$/i.test(executable);
65
+ if (windowsShim && [executable, ...args].some(part => /[&|<>^%!"()]/.test(part))) {
66
+ return { status: "unknown", reason: "the selected Windows command shim invocation cannot be probed safely" };
67
+ }
68
+ result = deps.spawn(
69
+ windowsShim ? (deps.env?.ComSpec ?? "cmd.exe") : executable,
70
+ windowsShim ? ["/c", executable, ...args, "--version"] : [...args, "--version"],
71
+ { timeout: VERSION_PROBE_TIMEOUT_MS, encoding: "utf8", stdio: "pipe", windowsHide: true },
72
+ ) as SpawnSyncReturns<string>;
73
+ } catch (error) {
74
+ return {
75
+ status: "unknown",
76
+ reason: `${identity} --version could not run: ${error instanceof Error ? error.message : String(error)}`,
77
+ };
78
+ }
79
+ if (result.error) {
80
+ return {
81
+ status: "unknown",
82
+ reason: `${identity} --version did not finish: ${result.error.message}`,
83
+ };
84
+ }
85
+ if (result.status !== 0) {
86
+ return { status: "unknown", reason: `${identity} --version exited ${result.status ?? "without a code"}` };
87
+ }
88
+ const version = versionFromOutput(result.stdout ?? "");
89
+ return version === null
90
+ ? { status: "unknown", reason: `${identity} --version printed no semver` }
91
+ : { status: "observed", version, identity };
92
+ }
93
+
94
+ /**
95
+ * Where `ocx` resolves on PATH without spawning `where`/`which`.
96
+ *
97
+ * Returns the absolute candidate or null. On Windows every PATHEXT extension is tried (and
98
+ * the bare name, for extensionless shims); elsewhere the bare name only.
99
+ */
100
+ function findOcxOnPath(
101
+ env: Record<string, string | undefined>,
102
+ exists: (path: string) => boolean,
103
+ platform: NodeJS.Platform,
104
+ ): string | null {
105
+ const pathValue = env.PATH ?? env.Path ?? env.path;
106
+ if (!pathValue) return null;
107
+ const pathApi = platform === "win32" ? win32 : posix;
108
+ const extensions = platform === "win32"
109
+ ? [...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(ext => /^\.[A-Za-z0-9]+$/.test(ext)), ""]
110
+ : [""];
111
+ for (const directory of pathValue.split(platform === "win32" ? ";" : delimiter)) {
112
+ if (!directory) continue;
113
+ for (const extension of extensions) {
114
+ const candidate = pathApi.join(directory, `ocx${extension}`);
115
+ if (exists(candidate)) return pathApi.resolve(candidate);
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function observeServiceRegistration(
122
+ state: ServiceInstallState | null,
123
+ deps: Required<Pick<ManagingCliDeps, "spawn" | "platform" | "env">>,
124
+ ): ManagingCliObservation {
125
+ const invocation = registeredManagingCliInvocation(state);
126
+ if (invocation.status !== "resolved") return invocation;
127
+ return probeVersion(invocation.executable, invocation.args, deps);
128
+ }
129
+
130
+ function observePathCli(
131
+ deps: Required<Pick<ManagingCliDeps, "spawn" | "platform" | "env" | "execPath" | "ownVersion" | "exists" | "isFile">>,
132
+ ): ManagingCliObservation {
133
+ const found = findOcxOnPath(deps.env, deps.exists, deps.platform);
134
+ if (!found) return { status: "absent" };
135
+ if (!deps.isFile(found)) {
136
+ return { status: "unknown", reason: "the selected managing CLI is not a readable file" };
137
+ }
138
+ const self = (deps.platform === "win32" ? win32 : posix).resolve(deps.execPath);
139
+ if (found === self || (deps.platform === "win32" && found.toLowerCase() === self.toLowerCase())) {
140
+ // Never spawn ourselves for our own version: the answer is already in hand, and the
141
+ // recursion that produced it is the #5418 regression.
142
+ return { status: "observed", version: deps.ownVersion(), identity: found };
143
+ }
144
+ return probeVersion(found, [], deps);
145
+ }
146
+
147
+ /**
148
+ * Both managing-CLI observations, for `assessServiceTakeoverCompatibility` and for the
149
+ * revalidation `recordServiceOwner` runs inside its lock.
150
+ */
151
+ export function observeManagingClis(
152
+ state: ServiceInstallState | null,
153
+ deps: ManagingCliDeps = {},
154
+ ): Readonly<Record<ManagingCliRole, ManagingCliObservation>> {
155
+ const resolved = {
156
+ spawn: deps.spawn ?? spawnSync,
157
+ platform: deps.platform ?? process.platform,
158
+ env: deps.env ?? (process.env as Record<string, string | undefined>),
159
+ execPath: deps.execPath ?? process.execPath,
160
+ ownVersion: deps.ownVersion ?? packageVersion,
161
+ exists: deps.exists ?? existsSync,
162
+ isFile: deps.isFile ?? ((path: string) => {
163
+ try { return statSync(path).isFile(); } catch { return false; }
164
+ }),
165
+ };
166
+ return {
167
+ "service-registration": observeServiceRegistration(state, resolved),
168
+ path: observePathCli(resolved),
169
+ };
170
+ }
@@ -172,7 +172,8 @@ export async function proxyStillLiveAfterStop(deps: {
172
172
  const probeDeadline = canRespawn
173
173
  ? deadline
174
174
  : now() + (SERVICE_STOP_LIVENESS.timeoutMs! * SERVICE_STOP_LIVENESS.attempts! + 250);
175
- return findLiveProxy({ ...SERVICE_STOP_LIVENESS, deadlineAt: probeDeadline, nowFn: now });
175
+ // A package-tree-fenced proxy (#5496) still holds the port; stop must not call it gone.
176
+ return findLiveProxy({ ...SERVICE_STOP_LIVENESS, deadlineAt: probeDeadline, nowFn: now, acceptPackageTreeFenced: true });
176
177
  });
177
178
  for (;;) {
178
179
  try {
@@ -205,6 +206,7 @@ async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
205
206
  const live = await findLiveProxy({
206
207
  ...SERVICE_STOP_LIVENESS,
207
208
  deadlineAt: Date.now() + 7000,
209
+ acceptPackageTreeFenced: true,
208
210
  });
209
211
  const liveKillPid = verifiedKillTarget(live?.pid);
210
212
  if (liveKillPid !== null) {
@@ -857,6 +857,10 @@ export function serviceHomeMatches(a: string, b: string): boolean {
857
857
  return normalizePathForCompare(a) === normalizePathForCompare(b);
858
858
  }
859
859
 
860
+ export function serviceCodexHomeMatchesInstall(recordedHome: string, deps: CodexHomeDeps = {}): boolean {
861
+ return serviceHomeMatches(recordedHome, currentCodexHome(deps));
862
+ }
863
+
860
864
  /** Single accessor for backend-sensitive service code — v1/legacy state maps to scheduler. */
861
865
  export function readServiceBackend(): ServiceBackend {
862
866
  return readServiceInstallState()?.backend === "native" ? "native" : "scheduler";
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { execSync } from "node:child_process";
2
+ import { execFileSync, execSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, unlinkSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
@@ -199,6 +199,21 @@ export function stopSystemd(): void { try { sh(`systemctl --user stop ${TASK}`);
199
199
 
200
200
  export function statusSystemd(): string { try { return sh(`systemctl --user status ${TASK}`); } catch { return ""; } }
201
201
 
202
+ /** Strict user-unit inactivity proof for guarded desktop takeover. */
203
+ export function probeSystemdUnitInactive(deps: { show?: () => string } = {}): "inactive" | "active" | "unknown" {
204
+ try {
205
+ const output = (deps.show ?? (() => execFileSync("systemctl", ["--user", "show", "-p", "ActiveState", "-p", "MainPID", TASK], {
206
+ encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "pipe"],
207
+ })))();
208
+ const active = systemdProperty(output, "ActiveState")?.toLowerCase();
209
+ const pid = systemdProperty(output, "MainPID");
210
+ if (active === "inactive" && pid === "0") return "inactive";
211
+ if (["active", "activating", "deactivating", "reloading"].includes(active ?? "")
212
+ || (pid !== null && /^\d+$/.test(pid) && Number(pid) > 0)) return "active";
213
+ return "unknown";
214
+ } catch { return "unknown"; }
215
+ }
216
+
202
217
  export function uninstallSystemd(deps: {
203
218
  run?: (command: string) => string;
204
219
  unitExists?: () => boolean;
package/src/service.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  export type { ServiceBackend, ServiceInstallState, ServiceStateEvidence, ServiceStateResolution, ServiceOwner, ServiceOwnership, ServiceOwnershipSubject, ServiceOwnershipResolution, ServiceStateSwapDeps, RecordServiceOwnerRequest, RecordServiceOwnerDeps, ReleaseServiceOwnerDeps, RemoveServiceStateDeps } from "./service/state";
10
- export { SERVICE_MANAGED_ENV, SERVICE_OWNERSHIP_PROTOCOL_VERSION, SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION, stableLauncherEntry, serviceLogPath, serviceStatePaths, serviceStatePathsForOpenCodexHome, parseServiceInstallState, parseServiceOwnership, inspectServiceStateEvidence, resolveServiceState, currentServiceHomes, serviceHomeMatches, readServiceBackend, serviceReinstallArgs, serviceInstallArgs, ServiceStateConflictError, ServiceOwnershipSubjectMismatchError, ServiceOwnershipSubjectUnknownError, ServiceTakeoverCompatibilityChangedError, swapServiceInstallState, removeServiceInstallStateRecords, serviceOwnership, resolveServiceOwnership, sameServiceOwnershipSubject, desktopOwnsService, ownershipGrantedTo, recordServiceOwner, releaseServiceOwner } from "./service/state";
10
+ export { SERVICE_MANAGED_ENV, SERVICE_OWNERSHIP_PROTOCOL_VERSION, SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION, stableLauncherEntry, serviceLogPath, serviceStatePaths, serviceStatePathsForOpenCodexHome, parseServiceInstallState, parseServiceOwnership, inspectServiceStateEvidence, resolveServiceState, currentServiceHomes, serviceHomeMatches, serviceCodexHomeMatchesInstall, readServiceBackend, serviceReinstallArgs, serviceInstallArgs, ServiceStateConflictError, ServiceOwnershipSubjectMismatchError, ServiceOwnershipSubjectUnknownError, ServiceTakeoverCompatibilityChangedError, swapServiceInstallState, removeServiceInstallStateRecords, serviceOwnership, resolveServiceOwnership, sameServiceOwnershipSubject, desktopOwnsService, ownershipGrantedTo, recordServiceOwner, releaseServiceOwner } from "./service/state";
11
11
  export type { OwnershipMutationLeaseOptions, OwnershipMutationLease } from "./service/ownership-mutation-lease.mjs";
12
12
  export { acquireOwnershipMutationLease, withOwnershipMutationLease } from "./service/ownership-mutation-lease.mjs";
13
13
  export type { ManagingCliRole, ManagingCliObservation, RegisteredManagingCliInvocation, ServiceTakeoverCompatibilityInput, ServiceTakeoverCompatibility } from "./service/ownership-compatibility";
@@ -924,6 +924,12 @@ export interface OcxConfig {
924
924
  * which has no row, can be ordered too. Range -100..100.
925
925
  */
926
926
  codexAccountPriorities?: Record<string, number>;
927
+ /**
928
+ * Per-account proactive-switch threshold overrides. Missing account entry inherits
929
+ * `autoSwitchThreshold`; 0 disables usage-driven switching only for that account.
930
+ * Includes the synthetic `__main__` Desktop account. Range 0..100.
931
+ */
932
+ codexAccountAutoSwitchThresholds?: Record<string, number>;
927
933
  /**
928
934
  * Account id the operator last selected by hand. Suppresses upward priority
929
935
  * preemption until that account crosses the auto-switch threshold. Stores the
@@ -1017,6 +1023,8 @@ export interface OcxConfig {
1017
1023
  activeCodexAccountId?: string;
1018
1024
  /** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
1019
1025
  autoSwitchThreshold?: number;
1026
+ /** Opt-in: return bound quota-strategy tasks to recovered higher-priority accounts. */
1027
+ codexAccountPriorityFailback?: boolean;
1020
1028
  /** New-session account rotation strategy for the Codex pool. Default quota (today's behaviour). */
1021
1029
  accountPoolStrategy?: OcxAccountPoolRotationStrategy | "reset-first";
1022
1030
  /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
@@ -242,6 +242,12 @@ export interface TierObservationContext {
242
242
  * preserving the behaviour for the public API where the echo does mean what it says.
243
243
  */
244
244
  responseTierAuthoritative?: boolean;
245
+ /**
246
+ * Set when the upstream refused the fast wire earlier in this request and the proxy resent at
247
+ * standard speed (Anthropic `speed: "fast"` without entitlement). The resend's outcome is then
248
+ * a `response-declined` downgrade rather than an unavailable wire.
249
+ */
250
+ upstreamDeclinedFast?: boolean;
245
251
  }
246
252
 
247
253
  export type TierDecision =
@@ -950,6 +956,16 @@ export interface OcxProviderConfig {
950
956
  * thinking separately in `reasoning_content` / `reasoning_details` instead of visible content.
951
957
  */
952
958
  reasoningSplitModels?: string[];
959
+ /**
960
+ * Model ids served by a gateway that runs no server-side reasoning parser, so a thinking model
961
+ * leaves its chain of thought inline in `content` as `<think>` / `<thinking>` / `<reasoning>`
962
+ * blocks and never sends `reasoning_content` or `reasoning_details`. Without this the whole
963
+ * chain of thought renders as the answer. The openai-chat adapter then splits those blocks back
964
+ * into reasoning. Off by default and narrow on purpose: 66 registry providers share this
965
+ * adapter, and a gateway that does parse reasoning must not have its visible content rewritten.
966
+ * Prefer a provider-side parser or `reasoningSplitModels` when the upstream supports either.
967
+ */
968
+ inlineThinkTagModels?: string[];
953
969
  /**
954
970
  * Model ids whose chat endpoint carries thinking as a structured `reasoning_details` array
955
971
  * (MiniMax M-series with `reasoning_split`): stream deltas repeat each detail's `text` as a
@@ -90,6 +90,16 @@ export interface OcxParsedRequest {
90
90
  * prepareOpaqueBlobRecovery after an authoritative rejection; consumers strip replayed blobs.
91
91
  */
92
92
  _stripReasoningEncryptedContent?: boolean;
93
+ /**
94
+ * Set when replayed reasoning item ids name items in a store this destination cannot read: by
95
+ * prepareOpaqueBlobRecovery before the one recovery rebuild, and by bindRouteReasoningReplayScope
96
+ * while the rejection memo is live or after a proven switch to a different destination or
97
+ * credential. The Responses passthrough then removes the `id` of every replayed reasoning item,
98
+ * whether or not it carries a blob. A stateful destination resolves a replayed id against its own
99
+ * store, so keeping it turns the send into `Item with id 'rs_…' not found` (#5583). A model
100
+ * change on the same destination and credential does not set this.
101
+ */
102
+ _dropForeignReasoningItemIds?: boolean;
93
103
  /** Final-route opt-in: emit v2 collaboration message arguments as plaintext on ChatGPT. */
94
104
  _plaintextV2AgentMessages?: boolean;
95
105
  /**
@@ -129,6 +129,8 @@ export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set
129
129
  *
130
130
  * Rewrites invented `default.<name>` prefixes back to a declared bare tool when that bare tool
131
131
  * is declared and neither `default.<name>` nor `default__<name>` was explicitly declared (#4176).
132
+ * The same wrapper may surround an already-flattened namespace identity; accept that exact
133
+ * declared suffix without treating its child name as a bare declaration.
132
134
  * Also normalizes legacy helper names (`exec_command`, `shell_command`, `apply_patch`, `view_image`) to
133
135
  * `exec` when code-mode `exec` is declared in the request catalog.
134
136
  *
@@ -151,7 +153,13 @@ export function normalizeDeclaredToolName(
151
153
  const bareDeclared = declaredBare ?? declared;
152
154
  if (
153
155
  bare.length > 0
154
- && bareDeclared.has(bare)
156
+ && (
157
+ bareDeclared.has(bare)
158
+ // Muse can wrap the complete `namespace__tool` identity in `default.`. Requiring the
159
+ // exact flattened identity to be declared preserves the #4176 provenance boundary:
160
+ // `default.tool` still cannot borrow a namespaced tool's manufactured bare alias.
161
+ || (bare.includes("__") && declared.has(bare))
162
+ )
155
163
  && !declared.has("default." + bare)
156
164
  && !declared.has("default__" + bare)
157
165
  ) {