@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,149 @@
1
+ /** Credential-option redaction for CLI argument errors. Kept dependency-free so the CLI head can use it. */
2
+ /**
3
+ * Options whose VALUE is a credential (or can carry one), listed here so a parse
4
+ * error never prints one. `--headers` belongs on the list defensively: custom
5
+ * headers are documented as non-secret metadata and the validator rejects the
6
+ * standard credential names, but it cannot recognize an arbitrary one such as
7
+ * `X-My-Token`, so a parse error must not echo the value back either way.
8
+ *
9
+ * `takeOption` only understands `--flag value`. `--flag=value` therefore falls
10
+ * through to `rejectArgs`, which reports the offending argument verbatim — for
11
+ * `--code=https://…?code=SECRET` that writes the authorization code to stderr,
12
+ * which is the exact exposure the stdin path exists to avoid.
13
+ */
14
+ const SECRET_OPTIONS = [
15
+ "--code",
16
+ "--headers",
17
+ "--api-key",
18
+ "--key",
19
+ "--secret",
20
+ "--password",
21
+ "--token",
22
+ "--admin-token",
23
+ "--pairing-code",
24
+ "--credential-env",
25
+ "--admin-token-env",
26
+ "--pairing-code-env",
27
+ ];
28
+
29
+ function isSecretOptionToken(token: string): boolean {
30
+ return SECRET_OPTIONS.includes(token) || SECRET_OPTIONS.some(option => token.startsWith(`${option}=`));
31
+ }
32
+
33
+ let credentialArgvSeen = false;
34
+ let credentialValues: string[] = [];
35
+ let consoleScrubInstalled = false;
36
+
37
+ /** Operands of credential options in argv, longest first; very short values are not scrubbed. */
38
+ function credentialOperands(argv: readonly string[]): string[] {
39
+ const values = new Set<string>();
40
+ for (let index = 0; index < argv.length; index++) {
41
+ const token = argv[index] as string;
42
+ const inline = SECRET_OPTIONS.find(option => token.startsWith(`${option}=`));
43
+ let value: string | undefined;
44
+ if (inline) value = token.slice(inline.length + 1);
45
+ else if (SECRET_OPTIONS.includes(token)) {
46
+ value = argv[argv[index + 1] === "--" ? index + 2 : index + 1];
47
+ // A following bare credential option is not this option's operand; its own operand is
48
+ // collected on its turn. An inline value is always an operand, whatever it looks like.
49
+ if (value !== undefined && SECRET_OPTIONS.includes(value)) value = undefined;
50
+ }
51
+ if (value !== undefined && value.length >= 4) values.add(value);
52
+ }
53
+ return [...values].sort((a, b) => b.length - a.length);
54
+ }
55
+
56
+ /** Replace every argv credential operand in text printed by this process. */
57
+ export function scrubCredentialOperands(text: string): string {
58
+ let out = text;
59
+ // Case-insensitive: some parsers lowercase an action before echoing it.
60
+ for (const value of credentialValues) out = out.replace(new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "<redacted>");
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Last-line guard for console diagnostics (stderr): parsers that echo a selector, an unknown
66
+ * subcommand or a leftover before any redaction would otherwise print an operand typed after
67
+ * a credential option. Installed once; it reads the operands recorded for the current argv.
68
+ */
69
+ function installConsoleScrub(): void {
70
+ if (consoleScrubInstalled) return;
71
+ consoleScrubInstalled = true;
72
+ // Diagnostics only: stdout carries JSON documents whose primitives must not be rewritten.
73
+ for (const method of ["warn", "error"] as const) {
74
+ const original = console[method].bind(console);
75
+ console[method] = (...args: unknown[]) => {
76
+ if (credentialValues.length === 0) return original(...args);
77
+ return original(...args.map(arg => typeof arg === "string" ? scrubCredentialOperands(arg)
78
+ : arg instanceof Error ? scrubCredentialOperands(arg.stack ?? arg.message) : arg));
79
+ };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Record whether the process argv carried any credential option. Parsers that take
85
+ * positionals can consume the option token itself (`ocx account use openai --token X`
86
+ * makes `--token` the id), leaving only its operand as a "leftover"; once a credential
87
+ * option was present anywhere, every bare leftover is reported as `<redacted>`.
88
+ */
89
+ export function noteCredentialArgv(argv: readonly string[]): void {
90
+ credentialArgvSeen = argv.some(isSecretOptionToken);
91
+ credentialValues = credentialOperands(argv);
92
+ if (credentialValues.length > 0) installConsoleScrub();
93
+ }
94
+
95
+ /**
96
+ * Replace credential values before they are reported back.
97
+ *
98
+ * Both spellings have to be covered, and the space-separated one spans two
99
+ * tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
100
+ * does not parse `--code` leaves the flag AND its value in the leftovers, and
101
+ * reporting them verbatim writes the credential to stderr. Repeating the
102
+ * option does the same with the second value, since the parser takes only the
103
+ * first occurrence.
104
+ *
105
+ * The token after the option is redacted whatever it looks like. Skipping
106
+ * `--`-prefixed tokens read as "that is a flag, not a value", but the shell
107
+ * hands over whatever was typed: `--code --SUPERSECRET` and
108
+ * `--code -- SUPERSECRET` both put the credential straight in the message. A
109
+ * mistaken `--code --json` now reads `--code <redacted>`, which is worse
110
+ * diagnostics for a case that already prints the usage text, and better than
111
+ * printing a credential.
112
+ *
113
+ * `redactValues` extends that to bare leftovers, for commands whose positional
114
+ * argument is itself a credential.
115
+ */
116
+ export function redactSecretArgs(args: string[], redactValues = false): string[] {
117
+ redactValues ||= credentialArgvSeen;
118
+ const out: string[] = [];
119
+ for (let index = 0; index < args.length; index++) {
120
+ const arg = args[index] as string;
121
+ const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
122
+ if (inline) {
123
+ out.push(`${inline}=<redacted>`);
124
+ continue;
125
+ }
126
+ if (SECRET_OPTIONS.includes(arg)) {
127
+ out.push(arg);
128
+ // Swallow the value that belongs to it. `--` is an end-of-options
129
+ // separator, so the value is the token after it.
130
+ let valueIndex = index + 1;
131
+ if (args[valueIndex] === "--") {
132
+ out.push("--");
133
+ valueIndex++;
134
+ }
135
+ const next = args[valueIndex];
136
+ // A following credential option is not this option's value: leave it for the
137
+ // next iteration so its own operand is redacted too (`--code --token SECRET`).
138
+ if (next !== undefined && !isSecretOptionToken(next)) {
139
+ out.push("<redacted>");
140
+ index = valueIndex;
141
+ } else if (next !== undefined) {
142
+ index = valueIndex - 1;
143
+ }
144
+ continue;
145
+ }
146
+ out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
147
+ }
148
+ return out;
149
+ }
@@ -0,0 +1,30 @@
1
+ import { printData } from "./runtime-api";
2
+ import type { CodexDesktopSwitchApply, CodexDesktopSwitchApplyReason } from "../codex/desktop-switches";
3
+
4
+ const APPLY_REASONS = {
5
+ not_requested: true, proxy_not_running: true, integration_disabled: true,
6
+ external_provider: true, ownership_undetermined: true, write_lock_busy: true, injection_refused: true,
7
+ } satisfies Record<CodexDesktopSwitchApplyReason, true>;
8
+
9
+ /** The shared native apply DTO, excluding exception/path detail. */
10
+ export function projectSettingsApply(value: unknown): CodexDesktopSwitchApply | null {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12
+ const row = value as Record<string, unknown>;
13
+ if (row.applied === true) return { applied: true };
14
+ if (row.applied !== false || typeof row.reason !== "string" || !Object.hasOwn(APPLY_REASONS, row.reason)
15
+ || typeof row.retryable !== "boolean") return null;
16
+ return { applied: false, reason: row.reason as CodexDesktopSwitchApplyReason, retryable: row.retryable };
17
+ }
18
+
19
+ /** Caller rebuilds the relevant task DTO; settings pending is not a CatalogDisposition. */
20
+ export function printSettingsResult(
21
+ data: Record<string, unknown>, pending: unknown, wantsJson: boolean, lines: readonly string[],
22
+ ): number {
23
+ const verified = typeof pending === "boolean";
24
+ const note = !verified ? "Settings were accepted; catalog status is unverified. Read back before retrying."
25
+ : pending ? "Settings saved; catalog refresh is pending."
26
+ : "Settings saved; this response reports no pending catalog refresh.";
27
+ printData({ ...data, catalogRefreshPending: verified ? pending : null,
28
+ ...(!verified ? { verification: "unverified" } : {}) }, wantsJson, [...lines, note]);
29
+ return verified && pending === false ? 0 : 1;
30
+ }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import {
19
19
  CliUsageError,
20
+ RuntimeApiError,
20
21
  printData,
21
22
  rejectArgs,
22
23
  runCliAction,
@@ -27,6 +28,7 @@ import {
27
28
  takeOption,
28
29
  type RuntimeApiDeps,
29
30
  } from "./runtime-api";
31
+ import type { StorageCleanupPolicy } from "../types/config";
30
32
 
31
33
  const USAGE = `Usage:
32
34
  ocx storage report [--json]
@@ -34,7 +36,8 @@ const USAGE = `Usage:
34
36
  ocx storage trash [list] [--json]
35
37
  ocx storage trash restore <entry-id> [--yes] [--json]
36
38
  ocx storage policy [show] [--json]
37
- ocx storage policy set [--enabled <true|false>] [--percent <0-100>]
39
+ ocx storage policy set [--enabled <true|false>] [--archived-bytes-over <bytes>]
40
+ [--reduce-to-bytes <bytes> | --remove-oldest-percent <1-100> | --percent <1-100>]
38
41
  [--mode <quarantine|permanent>] [--schedule <startup|daily|weekly|manual>] [--json]
39
42
  ocx storage policy run [--yes] [--json]
40
43
 
@@ -146,6 +149,41 @@ async function trash(argv: string[], deps: RuntimeApiDeps): Promise<void> {
146
149
  printData(result, wantsJson, summaryLines(result));
147
150
  }
148
151
 
152
+ /** New policy controls expose only observed settings and job status, never arbitrary API fields. */
153
+ type ObservedPolicy = Omit<StorageCleanupPolicy, "target"> & { target: { reduceToBytes: number } | { removeOldestPercent: number } };
154
+ function policyWriteReceipt(value: unknown): { ok: true; policy: ObservedPolicy; job: { status: "idle" | "running" } } {
155
+ const record = (item: unknown): item is Record<string, unknown> => item !== null && typeof item === "object" && !Array.isArray(item);
156
+ const count = (item: unknown): item is number => typeof item === "number" && Number.isInteger(item) && item >= 0;
157
+ const invalid = (): never => { throw new Error("Invalid storage policy response; inspect the policy before retrying."); };
158
+ if (!record(value) || value.ok !== true || !record(value.policy) || !record(value.job)) return invalid();
159
+ const source = value.policy;
160
+ if (typeof source.enabled !== "boolean" || !record(source.trigger) || !count(source.trigger.archivedBytesOver)
161
+ || !record(source.target) || (source.mode !== "quarantine" && source.mode !== "permanent")
162
+ || (source.schedule !== "startup" && source.schedule !== "daily" && source.schedule !== "weekly" && source.schedule !== "manual")
163
+ || (value.job.status !== "idle" && value.job.status !== "running")) return invalid();
164
+ const { reduceToBytes, removeOldestPercent } = source.target;
165
+ let target: ObservedPolicy["target"];
166
+ if (count(reduceToBytes) && removeOldestPercent === undefined) target = { reduceToBytes };
167
+ else if (count(removeOldestPercent) && removeOldestPercent >= 1 && removeOldestPercent <= 100 && reduceToBytes === undefined) target = { removeOldestPercent };
168
+ else return invalid();
169
+ const policy: ObservedPolicy = {
170
+ enabled: source.enabled,
171
+ trigger: { archivedBytesOver: source.trigger.archivedBytesOver },
172
+ target,
173
+ mode: source.mode,
174
+ schedule: source.schedule,
175
+ };
176
+ if (source.lastRun !== undefined) {
177
+ if (!record(source.lastRun) || !count(source.lastRun.at) || !count(source.lastRun.freedBytes) || !count(source.lastRun.removed)) return invalid();
178
+ policy.lastRun = { at: source.lastRun.at, freedBytes: source.lastRun.freedBytes, removed: source.lastRun.removed };
179
+ }
180
+ if (source.nextRun !== undefined) {
181
+ if (!count(source.nextRun)) return invalid();
182
+ policy.nextRun = source.nextRun;
183
+ }
184
+ return { ok: true, policy, job: { status: value.job.status } };
185
+ }
186
+
149
187
  async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
188
  const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "show";
151
189
  const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
@@ -164,6 +202,9 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
164
202
  const wantsJson = takeFlag(args, "--json");
165
203
  const enabled = takeOption(args, "--enabled");
166
204
  const percent = takeIntegerOption(args, "--percent", { min: 0 });
205
+ const removeOldestPercent = takeIntegerOption(args, "--remove-oldest-percent", { min: 0 });
206
+ const reduceToBytes = takeIntegerOption(args, "--reduce-to-bytes", { min: 0 });
207
+ const archivedBytesOver = takeIntegerOption(args, "--archived-bytes-over", { min: 0 });
167
208
  const mode = takeOption(args, "--mode");
168
209
  const schedule = takeOption(args, "--schedule");
169
210
  rejectArgs(args, USAGE);
@@ -171,8 +212,13 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
171
212
  if (enabled !== undefined && enabled !== "true" && enabled !== "false") {
172
213
  throw new CliUsageError("--enabled must be true or false", USAGE);
173
214
  }
215
+ if ([percent, removeOldestPercent, reduceToBytes].filter(value => value !== undefined).length > 1) {
216
+ throw new CliUsageError("choose one target: --percent, --remove-oldest-percent or --reduce-to-bytes", USAGE);
217
+ }
218
+ const extended = archivedBytesOver !== undefined || reduceToBytes !== undefined || removeOldestPercent !== undefined;
174
219
  const body: Record<string, unknown> = {};
175
220
  if (enabled !== undefined) body.enabled = enabled === "true";
221
+ if (archivedBytesOver !== undefined) body.trigger = { archivedBytesOver };
176
222
  // The policy target is nested. A top-level `percent` is not part of the PUT contract:
177
223
  // `normalizeStorageCleanupPolicy` reads only `target`, so the field was dropped and the
178
224
  // previously stored target survived. `--percent 10` on a policy still holding the
@@ -183,20 +229,44 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
183
229
  // vocabulary and answers with a named 400, which is a rejected write rather than the
184
230
  // silent wrong write this replaces.
185
231
  if (percent !== undefined) body.target = { removeOldestPercent: percent };
232
+ if (removeOldestPercent !== undefined) body.target = { removeOldestPercent };
233
+ if (reduceToBytes !== undefined) body.target = { reduceToBytes };
186
234
  if (mode !== undefined) body.mode = mode;
187
235
  if (schedule !== undefined) body.schedule = schedule;
188
236
  if (Object.keys(body).length === 0) {
189
- throw new CliUsageError("policy set needs at least one of --enabled, --percent, --mode, --schedule", USAGE);
237
+ throw new CliUsageError("policy set needs at least one policy field", USAGE);
190
238
  }
191
239
  // Values are NOT re-validated here beyond --enabled's shape. The server owns the mode and
192
240
  // schedule vocabularies and returns a named 400; duplicating them is a second thing to
193
241
  // keep in sync. `enabled` is checked because "--enabled maybe" would otherwise be sent as
194
242
  // `false`, which is a wrong write rather than a rejected one.
195
- const result = await runtimeRequest("/api/storage/cleanup-policy", {
196
- method: "PUT",
197
- headers: { "content-type": "application/json" },
198
- body: JSON.stringify(body),
199
- }, deps);
243
+ let result: unknown;
244
+ try {
245
+ result = await runtimeRequest("/api/storage/cleanup-policy", {
246
+ method: "PUT",
247
+ ...(extended ? { redirect: "error" as const } : {}),
248
+ headers: { "content-type": "application/json" },
249
+ body: JSON.stringify(body),
250
+ }, deps);
251
+ } catch (error) {
252
+ if (!extended) throw error;
253
+ if (error instanceof RuntimeApiError) {
254
+ throw new RuntimeApiError(`Storage policy request failed (${error.status}); inspect the policy before retrying.`, error.status, null);
255
+ }
256
+ throw new Error("Storage policy request failed; inspect the policy before retrying.");
257
+ }
258
+ if (extended) {
259
+ const receipt = policyWriteReceipt(result);
260
+ const saved = receipt.policy;
261
+ printData(receipt, wantsJson, [
262
+ "Policy saved. This command did not start cleanup.",
263
+ `Enabled: ${saved.enabled}`,
264
+ `Trigger: archived bytes over ${saved.trigger.archivedBytesOver}`,
265
+ "reduceToBytes" in saved.target ? `Target: reduce archives to ${saved.target.reduceToBytes} bytes` : `Target: remove oldest ${saved.target.removeOldestPercent}%`,
266
+ `Mode: ${saved.mode}; schedule: ${saved.schedule}; job: ${receipt.job.status}`,
267
+ ]);
268
+ return;
269
+ }
200
270
  printData(result, wantsJson, summaryLines(result));
201
271
  return;
202
272
  }
@@ -1,3 +1,4 @@
1
+ import { runCatalogAction } from "./catalog-command-result";
1
2
  import {
2
3
  CliUsageError,
3
4
  desktopSwitchApplyReason,
@@ -14,8 +15,11 @@ import {
14
15
 
15
16
  const USAGE = `Usage:
16
17
  ocx system [status] [--json]
18
+ ocx system health [--json]
17
19
  ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
18
- [--desktop-authless <on|off>] [--client-compaction <on|off>] [--json]
20
+ [--desktop-authless <on|off>] [--client-compaction <on|off>]
21
+ [--show-codex-credits <on|off>] [--account-picker <on|off>] [--main-account-hard-lock <on|off>]
22
+ [--ultra-fast-tier <on|off>] [--fast-rows <on|off>] [--json]
19
23
  ocx system startup <health|install-service|install-shim> [--json]
20
24
  ocx system diagnostics [--json]
21
25
  ocx system sync [--json]
@@ -45,6 +49,46 @@ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
45
49
  printData(result, wantsJson, summaryLines(result));
46
50
  }
47
51
 
52
+ /** The management health observation is distinct from root liveness and aggregate status. */
53
+ async function health(argv: string[], deps: RuntimeApiDeps): Promise<number> {
54
+ return runCatalogAction(async () => {
55
+ const args = [...argv], wantsJson = takeFlag(args, "--json");
56
+ if (args.length) throw new CliUsageError("system health accepts only --json", USAGE);
57
+ const raw = await runtimeRequest("/api/system/health", { method: "GET", redirect: "error" }, deps);
58
+ const value = recordValue(raw), ledger = recordValue(value?.spendLedger);
59
+ const counter = (number: unknown): number => {
60
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0 || number > 1_000_000) {
61
+ throw new Error("Invalid health counter");
62
+ }
63
+ return number;
64
+ };
65
+ if (!value || Array.isArray(raw) || !ledger || Array.isArray(value.spendLedger)
66
+ || value.status !== "ok" || value.service !== "opencodex"
67
+ || typeof value.version !== "string" || !value.version.trim()
68
+ || typeof value.uptime !== "number" || !Number.isFinite(value.uptime) || value.uptime < 0
69
+ || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
70
+ || (ledger.ownership !== "held" && ledger.ownership !== "unheld")
71
+ || typeof ledger.initialized !== "boolean" || typeof ledger.configured !== "boolean"
72
+ || typeof ledger.degraded !== "boolean") throw new Error("Invalid system health response");
73
+ const result = {
74
+ status: "ok", service: "opencodex", version: value.version, uptime: value.uptime, pid: value.pid,
75
+ spendLedger: {
76
+ ownership: ledger.ownership, initialized: ledger.initialized, configured: ledger.configured,
77
+ degraded: ledger.degraded, persistFailures: counter(ledger.persistFailures), corruptRecords: counter(ledger.corruptRecords),
78
+ },
79
+ };
80
+ printData(result, wantsJson, [
81
+ `Management endpoint: ${result.status} (${result.service} ${result.version}, PID ${result.pid})`,
82
+ `Uptime: ${result.uptime} seconds`,
83
+ `Spend ledger: ${ledger.degraded ? "degraded" : "no degradation reported"}; ownership ${ledger.ownership}`,
84
+ `Ledger initialized: ${ledger.initialized}; configured: ${ledger.configured}`,
85
+ `Ledger persistence failures: ${result.spendLedger.persistFailures}; corrupt records: ${result.spendLedger.corruptRecords}`,
86
+ "This observation does not certify every subsystem healthy.",
87
+ ...(ledger.degraded ? ["Next: inspect the proxy diagnostics and spend-ledger configuration before relying on spend limits."] : []),
88
+ ]);
89
+ });
90
+ }
91
+
48
92
  function recordValue(value: unknown): Record<string, unknown> | undefined {
49
93
  return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
50
94
  }
@@ -187,11 +231,25 @@ async function update(argv: string[], deps: RuntimeApiDeps): Promise<void> {
187
231
  if (!yes) throw new CliUsageError("update run requires --yes", USAGE);
188
232
  rejectArgs(args, USAGE);
189
233
  const result = await runtimeRequest("/api/update/run", { method: "POST", body: JSON.stringify({ tag: channel, restart }) }, deps);
190
- printData(result, wantsJson, [`Update started (${channel}).`]);
234
+ const job = recordValue(recordValue(result)?.job);
235
+ const jobId = typeof job?.id === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(job.id) ? job.id : undefined;
236
+ const jobStatus = ["running", "restarting", "succeeded", "failed"].find(state => state === job?.status);
237
+ const lines = jobId ? [
238
+ `Update job ${jobId}: ${jobStatus ?? "status unconfirmed"} (${channel}).`,
239
+ `Check progress: ocx system update status ${jobId}`,
240
+ ] : ["Update request returned no usable job ID. Check: ocx system update check."];
241
+ printData(result, wantsJson, lines);
191
242
  }
192
243
 
193
244
  export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
194
245
  const [sub = "status", ...rest] = argv;
246
+ if (sub === "health") return health(rest, deps);
247
+ if (sub === "settings") {
248
+ const { SYSTEM_PARITY_OPTIONS, handleSystemSettingsParity } = await import("./system-settings-parity");
249
+ if (rest.some(arg => SYSTEM_PARITY_OPTIONS.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
250
+ return handleSystemSettingsParity(rest, deps);
251
+ }
252
+ }
195
253
  if (sub === "codex-cli-update") {
196
254
  const { handleCodexCliUpdateCommand } = await import("./codex-cli-update");
197
255
  return await handleCodexCliUpdateCommand(rest);
@@ -26,6 +26,8 @@ import {
26
26
  import type { ProxyRestartRequestOutcome } from "./tray-proxy";
27
27
  import { packageVersion } from "./help";
28
28
  import { computeVersionSkew } from "./version-skew";
29
+ import { readUpdateRestartHome } from "./update-restart-home";
30
+ import { UpdateRestartRequired } from "./update-restart-candidate";
29
31
 
30
32
  export const SYSTEM_RESTART_REQUEST_TIMEOUT_MS = 5_000;
31
33
  export const SYSTEM_RESTART_ATTESTATION_TIMEOUT_MS = 4_000;
@@ -38,6 +40,7 @@ export interface BoundSystemRestartDeps {
38
40
  now?: () => number;
39
41
  /** Invoking CLI version for the skew guard; defaults to this bundle's package version. */
40
42
  cliVersion?: string;
43
+ readUpdateHome?: typeof readUpdateRestartHome;
41
44
  }
42
45
 
43
46
  function rejected(code: string): ProxyRestartRequestOutcome {
@@ -90,6 +93,8 @@ export async function requestBoundSystemRestart(
90
93
  return rejected("restart_target_runtime_mismatch");
91
94
  }
92
95
 
96
+ let updateHome: ReturnType<typeof readUpdateRestartHome> | undefined;
97
+ try { updateHome = (deps.readUpdateHome ?? readUpdateRestartHome)(); } catch { /* normal in-place restart remains available */ }
93
98
  const attestationBudget = remaining(deadlineAt, now, SYSTEM_RESTART_ATTESTATION_TIMEOUT_MS);
94
99
  if (attestationBudget <= 0) return rejected("restart_deadline_expired");
95
100
 
@@ -143,9 +148,9 @@ export async function requestBoundSystemRestart(
143
148
  const proxyVersion = fenced
144
149
  ? body.installedVersion as string
145
150
  : typeof body.version === "string" ? body.version : undefined;
146
- if (computeVersionSkew(deps.cliVersion ?? ownCliVersion(), proxyVersion).skewed) {
147
- return rejected("restart_version_skew");
148
- }
151
+ const cliVersion = deps.cliVersion ?? ownCliVersion();
152
+ const skew = computeVersionSkew(cliVersion, proxyVersion);
153
+ if (skew.skewed && skew.relation !== "cli-newer") return rejected("restart_version_skew");
149
154
 
150
155
  let observed: LiveProxy | null;
151
156
  try {
@@ -154,6 +159,18 @@ export async function requestBoundSystemRestart(
154
159
  return rejected("restart_target_recheck_failed");
155
160
  }
156
161
  if (!sameRestartTarget(target, observed)) return rejected("restart_target_changed");
162
+ if (skew.relation === "cli-newer") {
163
+ if (!updateHome) return rejected("update_restart_home_unverified");
164
+ const current = readRuntime(target.pid);
165
+ if (!current || current.pid !== runtime.pid || current.port !== runtime.port
166
+ || current.hostname !== runtime.hostname || current.attestationSecret !== runtime.attestationSecret) {
167
+ return rejected("restart_target_changed");
168
+ }
169
+ return { accepted: false, uncertain: false, error: new UpdateRestartRequired({
170
+ target: { ...target, pid: target.pid, version: proxyVersion },
171
+ runtime: { ...runtime, attestationSecret: runtime.attestationSecret }, cliVersion, home: updateHome,
172
+ }) };
173
+ }
157
174
 
158
175
  const capability = createSystemRestartCapability(
159
176
  runtime.attestationSecret,
@@ -0,0 +1,83 @@
1
+ import { runCatalogAction } from "./catalog-command-result";
2
+ import { serializeManagementJson } from "./json-input";
3
+ import { printSettingsResult, projectSettingsApply } from "./settings-result";
4
+ import { CliUsageError, runtimeBaseUrl, runtimeRequest, takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
5
+
6
+ export const SYSTEM_PARITY_OPTIONS = ["--show-codex-credits", "--account-picker", "--main-account-hard-lock", "--ultra-fast-tier", "--fast-rows"] as const;
7
+ const BOOLEAN_FIELDS: Readonly<Record<string, string>> = {
8
+ "--show-codex-credits": "showCodexCredits", "--account-picker": "codexAccountPickerEnabled",
9
+ "--main-account-hard-lock": "codexMainAccountHardLock", "--ultra-fast-tier": "ultraFastTier", "--fast-rows": "fastRows",
10
+ "--auto-start": "codexAutoStart", "--desktop-authless": "codexDesktopAuthless", "--client-compaction": "codexClientCompaction",
11
+ };
12
+ function record(value: unknown): value is Record<string, unknown> {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ function boolean(raw: string, option: string): boolean {
16
+ if (["on", "true", "yes", "1", "enabled"].includes(raw.toLowerCase())) return true;
17
+ if (["off", "false", "no", "0", "disabled"].includes(raw.toLowerCase())) return false;
18
+ throw new CliUsageError(`${option} must be on or off`);
19
+ }
20
+ function desktopState(value: unknown): Record<string, unknown> | null {
21
+ if (!record(value) || typeof value.stored !== "boolean" || (value.effective !== null && typeof value.effective !== "boolean")
22
+ || (value.inertReason !== undefined && value.inertReason !== "client_role" && value.inertReason !== "non_loopback_bind_requires_admission_token")) return null;
23
+ return { stored: value.stored, effective: value.effective, ...(value.inertReason ? { inertReason: value.inertReason } : {}) };
24
+ }
25
+
26
+ /** New-option writes; the original no-new-option settings output remains compatible. */
27
+ export async function handleSystemSettingsParity(argsInput: string[], deps: RuntimeApiDeps = {}): Promise<number> {
28
+ return runCatalogAction(async () => {
29
+ const args = [...argsInput], wantsJson = takeFlag(args, "--json");
30
+ const body: Record<string, unknown> = {};
31
+ for (const [option, field] of Object.entries(BOOLEAN_FIELDS)) {
32
+ const value = takeOptionWithSyntax(args, option)?.value;
33
+ if (value !== undefined) body[field] = boolean(value, option);
34
+ }
35
+ const mode = takeOptionWithSyntax(args, "--stream-mode")?.value;
36
+ if (mode !== undefined) {
37
+ if (!["auto", "legacy-tee", "eager-relay"].includes(mode)) throw new CliUsageError("Invalid --stream-mode");
38
+ body.streamMode = mode;
39
+ }
40
+ if (args.length || !Object.keys(body).length) throw new CliUsageError("Provide supported system setting options and optional --json");
41
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
42
+ const response = await runtimeRequest("/api/settings", { method: "PUT", redirect: "error", body: serializeManagementJson(body) }, pinned);
43
+ if (!record(response) || response.ok !== true) throw new Error("Invalid settings receipt");
44
+ const observed: Record<string, unknown> = {};
45
+ const unverified: string[] = [];
46
+ for (const [key, requested] of Object.entries(body)) {
47
+ if (key === "ultraFastTier") continue;
48
+ const actual = response[key];
49
+ if (typeof actual === typeof requested && (key !== "streamMode" || ["auto", "legacy-tee", "eager-relay"].includes(actual as string))) observed[key] = actual;
50
+ if (actual !== requested) unverified.push(key);
51
+ }
52
+ if (Object.hasOwn(body, "ultraFastTier")) {
53
+ try {
54
+ const readBack = await runtimeRequest("/api/settings", { redirect: "error" }, pinned);
55
+ if (record(readBack) && typeof readBack.ultraFastTier === "boolean") observed.ultraFastTier = readBack.ultraFastTier;
56
+ } catch { /* Acceptance already happened; retain safe unknown verification, not raw error. */ }
57
+ if (observed.ultraFastTier !== body.ultraFastTier) unverified.push("ultraFastTier");
58
+ }
59
+ const data: Record<string, unknown> = { ok: true, settings: observed };
60
+ let nativePending = false;
61
+ if (Object.hasOwn(body, "codexDesktopAuthless") || Object.hasOwn(body, "codexClientCompaction")) {
62
+ const switches = record(response.codexDesktopSwitches) ? response.codexDesktopSwitches : {};
63
+ const report: Record<string, unknown> = {};
64
+ for (const key of ["codexDesktopAuthless", "codexClientCompaction"]) {
65
+ if (!Object.hasOwn(body, key)) continue;
66
+ const state = desktopState(switches[key]);
67
+ if (state) report[key] = state; else unverified.push(`${key}.effective`);
68
+ }
69
+ const apply = projectSettingsApply(switches.apply);
70
+ if (apply) {
71
+ report.apply = apply;
72
+ nativePending = !apply.applied && apply.reason !== "not_requested";
73
+ } else unverified.push("desktopApply");
74
+ data.desktop = report;
75
+ }
76
+ if (unverified.length) { data.verification = "unverified"; data.unverifiedFields = unverified; }
77
+ const lines = ["System settings accepted.", ...Object.entries(observed).map(([key, value]) => `${key}: ${value}`)];
78
+ if (unverified.length) lines.push(`Read-back is unverified for: ${unverified.join(", ")}.`);
79
+ if (nativePending) lines.push("Native client application is deferred or refused; the settings remain saved.");
80
+ const pendingCode = printSettingsResult(data, response.catalogRefreshPending, wantsJson, lines);
81
+ return unverified.length || nativePending ? 1 : pendingCode;
82
+ });
83
+ }
@@ -128,11 +128,12 @@ export function reobserveRestartReplacement(
128
128
  deadlineAt: number,
129
129
  observe: (end: number) => Promise<ProxyRestartDiscovery>,
130
130
  ): Promise<ProxyRestartDiscovery> {
131
- const windowMs = Math.min(5_000, deadlineAt - Date.now());
131
+ const now = Date.now();
132
+ const windowMs = Math.min(5_000, deadlineAt - now);
132
133
  if (windowMs < 1_500) return Promise.resolve({
133
134
  status: "uncertain", error: new Error("restart_reobserve_window_exhausted"),
134
135
  });
135
- const end = Date.now() + windowMs;
136
+ const end = now + windowMs;
136
137
  return pollReplacementDeparture(() => observe(end), previous, () => Date.now() + 750 < end, () => Bun.sleep(250));
137
138
  }
138
139
 
@@ -0,0 +1,21 @@
1
+ import type { RuntimePortState } from "../config/process-state";
2
+ import type { LiveProxy } from "../server/proxy-liveness";
3
+
4
+ import type { UpdateRestartHome } from "./update-restart-home";
5
+
6
+ export interface UpdateRestartCandidate {
7
+ home: UpdateRestartHome;
8
+ target: LiveProxy & { pid: number };
9
+ runtime: RuntimePortState & { attestationSecret: string };
10
+ cliVersion: string;
11
+ }
12
+
13
+ /** Private in-process evidence; diagnostics and JSON never expose the proof key. */
14
+ export class UpdateRestartRequired extends Error {
15
+ #candidate: UpdateRestartCandidate;
16
+ constructor(candidate: UpdateRestartCandidate) {
17
+ super("restart_version_skew_cli_newer");
18
+ this.#candidate = candidate;
19
+ }
20
+ candidate(): UpdateRestartCandidate { return this.#candidate; }
21
+ }