@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,1531 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Database } from "bun:sqlite";
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ constants as fsConstants,
7
+ existsSync,
8
+ fstatSync,
9
+ ftruncateSync,
10
+ lstatSync,
11
+ mkdirSync,
12
+ openSync,
13
+ readFileSync,
14
+ readdirSync,
15
+ realpathSync,
16
+ renameSync,
17
+ rmSync,
18
+ statSync,
19
+ unlinkSync,
20
+ writeFileSync,
21
+ } from "node:fs";
22
+ import { dirname, join, relative, resolve, sep } from "node:path";
23
+ import { atomicWriteFileAsync } from "../config";
24
+ import { hardenSecretDirAsync, hardenSecretPathAsync } from "../lib/windows-secret-acl";
25
+ import { applyConfirmedMainCodexAccountTransition } from "./account-lifecycle";
26
+ import {
27
+ assertStableLockFile,
28
+ openStableLockFile,
29
+ StableLockPathUnsafeError,
30
+ type StableLockFile,
31
+ } from "./native-main-lock-file";
32
+ import { decideNativeProfileRecovery } from "./native-profile-recovery";
33
+ import { probeNativeCodexProcesses, type NativeCodexProcessProbe } from "./native-profile-processes";
34
+ import {
35
+ assertUniqueNativeProfileLabel,
36
+ assertNativeProfileLockPath,
37
+ assertNativeProfileMetadataLayout,
38
+ assertNoLegacyNativeProfileState,
39
+ decryptNativeEnvelope,
40
+ encryptNativeEnvelope,
41
+ inspectNativeProfileJournal,
42
+ MAX_NATIVE_PROFILES,
43
+ nativeIdentityHash,
44
+ nativeIdentityHint,
45
+ nativeProfileSelectorKey,
46
+ OsNativeProfileKeyProvider,
47
+ publicNativeProfile,
48
+ readNativeEnvelope,
49
+ readNativeEnvelopeResult,
50
+ readNativeProfileVault,
51
+ probeNativeProfileRecoveryState,
52
+ requireFileCredentialStore,
53
+ resolveNativeCredentialStoreMode,
54
+ resolveNativeProfileContext,
55
+ serializeNativeProfileJournal,
56
+ serializeNativeProfileMetadata,
57
+ validateNativeProfileLabel,
58
+ type NativeEnvelopeSnapshot,
59
+ type NativeProfileContext,
60
+ } from "./native-profile-store";
61
+ import {
62
+ NativeProfileError,
63
+ type NativeMainProfileRecordV1,
64
+ type NativeMainProfileVaultV1,
65
+ type NativeProfileKey,
66
+ type NativeProfileKeyProvider,
67
+ type NativeProfilePublic,
68
+ type NativeProfileSwitchJournalV1,
69
+ } from "./native-profile-types";
70
+ import {
71
+ NATIVE_STAGE_HEARTBEAT_INTERVAL_MS,
72
+ NativeProfileStageStore,
73
+ type NativeStageProof,
74
+ type NativeStageTerminalOutcome,
75
+ } from "./native-profile-stage-store";
76
+
77
+ const LOCK_WAIT_MS = 5_000;
78
+ const LEGACY_STAGE_MAX_AGE_MS = 30 * 60_000;
79
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
80
+
81
+ interface PathIdentity {
82
+ dev: bigint;
83
+ ino: bigint;
84
+ }
85
+
86
+ type AtomicWriter = (path: string, content: string) => Promise<void>;
87
+ type TransitionApplier = (fromAccountId: string, toAccountId: string) => void;
88
+ type EnvelopeReader = (path: string) => NativeEnvelopeSnapshot;
89
+ type VaultReader = () => NativeMainProfileVaultV1 | null;
90
+ type EnvelopeResultReader = (path: string) => ReturnType<typeof readNativeEnvelopeResult>;
91
+ type StableLockOpener = (path: string) => StableLockFile;
92
+ type StableLockAsserter = (path: string, file: StableLockFile) => void;
93
+ export type NativeProfileSwitchBoundary =
94
+ | "journal-prepared"
95
+ | "auth-replaced"
96
+ | "vault-committed"
97
+ | "runtime-transition-published"
98
+ | "journal-deleted";
99
+
100
+ export interface NativeProfileManagerOptions {
101
+ codexHome?: string;
102
+ configDir?: string;
103
+ keyProvider?: NativeProfileKeyProvider;
104
+ atomicWrite?: AtomicWriter;
105
+ hardenPath?: (path: string) => Promise<void>;
106
+ processProbe?: () => Promise<NativeCodexProcessProbe>;
107
+ applyTransition?: TransitionApplier;
108
+ now?: () => number;
109
+ randomUUID?: () => string;
110
+ sleep?: (ms: number) => Promise<void>;
111
+ lockWaitMs?: number;
112
+ onLockAcquired?: () => void | Promise<void>;
113
+ removeStageTree?: (path: string) => void;
114
+ stageLeaseMs?: number;
115
+ /** Test-only crash seam; production never supplies this callback. */
116
+ onSwitchBoundary?: (boundary: NativeProfileSwitchBoundary) => void | Promise<void>;
117
+ /** Test-only reader seams; production uses the native profile store directly. */
118
+ readEnvelope?: EnvelopeReader;
119
+ readVault?: VaultReader;
120
+ readEnvelopeResult?: EnvelopeResultReader;
121
+ /** Test-only stable-path seams; production uses the native stable-lock helpers directly. */
122
+ stableLockOpen?: StableLockOpener;
123
+ stableLockAssert?: StableLockAsserter;
124
+ }
125
+
126
+ export interface NativeProfileListResult {
127
+ effectiveCodexHome: string;
128
+ activeProfileId: string | null;
129
+ profiles: NativeProfilePublic[];
130
+ }
131
+
132
+ export interface NativeStageCleanupResult {
133
+ removed: boolean;
134
+ plaintextMayRemain: boolean;
135
+ }
136
+
137
+ export interface NativeStageSweepResult {
138
+ removed: number;
139
+ live: number;
140
+ cleanupFailed: number;
141
+ plaintextMayRemain: boolean;
142
+ }
143
+
144
+ async function hardenNativeProfilePath(path: string): Promise<void> {
145
+ const entry = lstatSync(path);
146
+ if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
147
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "A native-profile path is not a private file or directory.", 409);
148
+ }
149
+ const directory = entry.isDirectory();
150
+ const mode = directory ? 0o700 : 0o600;
151
+ try { chmodSync(path, mode); } catch { /* Windows ACL below is authoritative there. */ }
152
+ if (process.platform === "win32") {
153
+ if (directory) await hardenSecretDirAsync(path, { required: true, timeoutMemoKey: path });
154
+ else await hardenSecretPathAsync(path, { required: true, timeoutMemoKey: path });
155
+ }
156
+ }
157
+
158
+ function samePath(left: string, right: string): boolean {
159
+ return resolve(left) === resolve(right);
160
+ }
161
+
162
+ function pathIdentity(path: string): PathIdentity {
163
+ const entry = lstatSync(path, { bigint: true });
164
+ return { dev: entry.dev, ino: entry.ino };
165
+ }
166
+
167
+ function sameIdentity(left: PathIdentity, right: PathIdentity): boolean {
168
+ return left.dev === right.dev && left.ino === right.ino;
169
+ }
170
+
171
+ function assertPathIdentity(path: string, expected: PathIdentity, label: string): void {
172
+ try {
173
+ if (!sameIdentity(pathIdentity(path), expected)) throw new Error("identity changed");
174
+ } catch {
175
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", `${label} was replaced during the native-profile operation.`, 409);
176
+ }
177
+ }
178
+
179
+ function assertSafeDirectory(path: string, label: string): string {
180
+ try {
181
+ const entry = lstatSync(path);
182
+ if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("entry type");
183
+ const canonical = realpathSync.native(path);
184
+ if (!samePath(canonical, path)) throw new Error("path substitution");
185
+ return canonical;
186
+ } catch {
187
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", `${label} is not a private canonical directory.`, 409);
188
+ }
189
+ }
190
+
191
+ function errorCode(error: unknown): string | undefined {
192
+ return error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : undefined;
193
+ }
194
+
195
+ export class NativeProfileManager {
196
+ readonly context: NativeProfileContext;
197
+ private readonly keyProvider: NativeProfileKeyProvider;
198
+ private readonly atomicWrite: AtomicWriter;
199
+ private readonly hardenPath: (path: string) => Promise<void>;
200
+ private readonly processProbe: () => Promise<NativeCodexProcessProbe>;
201
+ private readonly applyTransition: TransitionApplier;
202
+ private readonly now: () => number;
203
+ private readonly uuid: () => string;
204
+ private readonly sleep: (ms: number) => Promise<void>;
205
+ private readonly lockWaitMs: number;
206
+ private readonly onLockAcquired: () => void | Promise<void>;
207
+ private readonly removeStageTree: (path: string) => void;
208
+ private readonly onSwitchBoundary: (boundary: NativeProfileSwitchBoundary) => void | Promise<void>;
209
+ private readonly readEnvelope: EnvelopeReader;
210
+ private readonly readVault: VaultReader;
211
+ private readonly readEnvelopeResult: EnvelopeResultReader;
212
+ private readonly stableLockOpen: StableLockOpener;
213
+ private readonly stableLockAssert: StableLockAsserter;
214
+ private readonly stageStore: NativeProfileStageStore;
215
+ private activeRootIdentity: PathIdentity | null = null;
216
+ private activeLockFile: StableLockFile | null = null;
217
+
218
+ constructor(options: NativeProfileManagerOptions = {}) {
219
+ this.context = resolveNativeProfileContext(options);
220
+ this.keyProvider = options.keyProvider ?? new OsNativeProfileKeyProvider();
221
+ this.atomicWrite = options.atomicWrite ?? atomicWriteFileAsync;
222
+ this.hardenPath = options.hardenPath ?? hardenNativeProfilePath;
223
+ this.processProbe = options.processProbe ?? probeNativeCodexProcesses;
224
+ this.applyTransition = options.applyTransition ?? ((from, to) => { applyConfirmedMainCodexAccountTransition(from, to); });
225
+ this.now = options.now ?? Date.now;
226
+ this.uuid = options.randomUUID ?? randomUUID;
227
+ this.sleep = options.sleep ?? (ms => Bun.sleep(ms));
228
+ this.lockWaitMs = options.lockWaitMs ?? LOCK_WAIT_MS;
229
+ this.onLockAcquired = options.onLockAcquired ?? (() => {});
230
+ this.removeStageTree = options.removeStageTree ?? (path => rmSync(path, { recursive: true, force: false }));
231
+ this.onSwitchBoundary = options.onSwitchBoundary ?? (() => {});
232
+ this.readEnvelope = options.readEnvelope ?? readNativeEnvelope;
233
+ this.readVault = options.readVault ?? (() => readNativeProfileVault(this.context));
234
+ this.readEnvelopeResult = options.readEnvelopeResult ?? readNativeEnvelopeResult;
235
+ this.stableLockOpen = options.stableLockOpen ?? openStableLockFile;
236
+ this.stableLockAssert = options.stableLockAssert ?? assertStableLockFile;
237
+ this.stageStore = new NativeProfileStageStore({
238
+ context: this.context,
239
+ now: this.now,
240
+ randomUUID: this.uuid,
241
+ atomicWrite: this.atomicWrite,
242
+ hardenPath: this.hardenPath,
243
+ leaseMs: options.stageLeaseMs,
244
+ });
245
+ }
246
+
247
+ private async ensureRoot(): Promise<PathIdentity> {
248
+ assertNativeProfileMetadataLayout(this.context);
249
+ if (!existsSync(this.context.rootDir)) {
250
+ try { mkdirSync(this.context.rootDir, { mode: 0o700 }); }
251
+ catch (error) { if (errorCode(error) !== "EEXIST") throw error; }
252
+ }
253
+ const identity = pathIdentity(this.context.rootDir);
254
+ assertPathIdentity(this.context.rootDir, identity, "The native-profile metadata root");
255
+ await this.hardenPath(this.context.rootDir);
256
+ assertPathIdentity(this.context.rootDir, identity, "The native-profile metadata root");
257
+ assertNativeProfileMetadataLayout(this.context);
258
+ for (const name of readdirSync(this.context.rootDir)) {
259
+ assertPathIdentity(this.context.rootDir, identity, "The native-profile metadata root");
260
+ await this.hardenPath(join(this.context.rootDir, name));
261
+ assertPathIdentity(this.context.rootDir, identity, "The native-profile metadata root");
262
+ }
263
+ assertNativeProfileMetadataLayout(this.context);
264
+ assertPathIdentity(this.context.rootDir, identity, "The native-profile metadata root");
265
+ return identity;
266
+ }
267
+
268
+ private isLockBusy(error: unknown): boolean {
269
+ const code = errorCode(error);
270
+ const message = error instanceof Error ? error.message : String(error);
271
+ return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || /database (?:is|table is) locked/i.test(message);
272
+ }
273
+
274
+ private profileLockStorageUnsafe(): NativeProfileError {
275
+ return new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The native-profile transaction lock was replaced.", 409);
276
+ }
277
+
278
+ private profileLockUnavailable(): NativeProfileError {
279
+ return new NativeProfileError("PROFILE_LOCK_UNAVAILABLE", "The native-profile lock is unavailable.", 503, true);
280
+ }
281
+
282
+ private isExplicitProfileLockStorageUnsafe(error: unknown): boolean {
283
+ const code = errorCode(error);
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ return error instanceof StableLockPathUnsafeError
286
+ || code === "ELOOP"
287
+ || code === "EISDIR"
288
+ || code === "ENOTDIR"
289
+ || message.startsWith("unsafe SQLite lock path:")
290
+ || message.startsWith("SQLite lock path identity changed:");
291
+ }
292
+
293
+ private openStableProfileLockFile(): StableLockFile {
294
+ try {
295
+ return this.stableLockOpen(this.context.lockPath);
296
+ } catch (error) {
297
+ if (this.isExplicitProfileLockStorageUnsafe(error)) throw this.profileLockStorageUnsafe();
298
+ throw this.profileLockUnavailable();
299
+ }
300
+ }
301
+
302
+ private assertStableProfileLockFile(file: StableLockFile): void {
303
+ try {
304
+ this.stableLockAssert(this.context.lockPath, file);
305
+ } catch (error) {
306
+ if (errorCode(error) === "ENOENT" || this.isExplicitProfileLockStorageUnsafe(error)) {
307
+ throw this.profileLockStorageUnsafe();
308
+ }
309
+ throw this.profileLockUnavailable();
310
+ }
311
+ }
312
+
313
+ private async withLock<T>(operation: () => Promise<T>): Promise<T> {
314
+ assertNoLegacyNativeProfileState(this.context);
315
+ assertNativeProfileLockPath(this.context);
316
+ const deadline = this.now() + this.lockWaitMs;
317
+ let database: Database | undefined;
318
+ let lockFile: StableLockFile | undefined;
319
+ while (!database) {
320
+ let candidate: Database | undefined;
321
+ let candidateFile: StableLockFile | undefined;
322
+ try {
323
+ candidateFile = this.openStableProfileLockFile();
324
+ this.assertStableProfileLockFile(candidateFile);
325
+ candidate = new Database(this.context.lockPath, { create: true });
326
+ const claim = this.uuid();
327
+ candidate.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
328
+ candidate.exec("CREATE TABLE IF NOT EXISTS ocx_native_profile_lock (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), claim TEXT NOT NULL)");
329
+ candidate.query("INSERT INTO ocx_native_profile_lock (singleton, claim) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET claim = excluded.claim").run(claim);
330
+ candidate.exec("COMMIT");
331
+ candidate.exec("BEGIN IMMEDIATE");
332
+ const candidateClaim = candidate.query("SELECT claim FROM ocx_native_profile_lock WHERE singleton = 1").get() as { claim?: unknown } | null;
333
+ let verifier: Database | undefined;
334
+ let pathClaim: { claim?: unknown } | null = null;
335
+ try {
336
+ this.assertStableProfileLockFile(candidateFile);
337
+ verifier = new Database(this.context.lockPath, { readonly: true });
338
+ pathClaim = verifier.query("SELECT claim FROM ocx_native_profile_lock WHERE singleton = 1").get() as { claim?: unknown } | null;
339
+ } finally {
340
+ try { verifier?.close(); } catch { /* mismatch below is authoritative */ }
341
+ }
342
+ this.assertStableProfileLockFile(candidateFile);
343
+ if (candidateClaim?.claim !== claim || pathClaim?.claim !== claim) {
344
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The native-profile transaction lock changed during acquisition.", 409);
345
+ }
346
+ try { chmodSync(this.context.lockPath, 0o600); } catch { /* Windows ACL below is authoritative there. */ }
347
+ await this.hardenPath(this.context.lockPath);
348
+ assertNativeProfileLockPath(this.context);
349
+ this.assertStableProfileLockFile(candidateFile);
350
+ database = candidate;
351
+ candidate = undefined;
352
+ lockFile = candidateFile;
353
+ candidateFile = undefined;
354
+ } catch (error) {
355
+ let mappedError = error;
356
+ if (!(mappedError instanceof NativeProfileError) && candidateFile) {
357
+ try { this.assertStableProfileLockFile(candidateFile); }
358
+ catch (storageError) { mappedError = storageError; }
359
+ }
360
+ try { candidate?.exec("ROLLBACK"); } catch { /* acquisition already failed */ }
361
+ try { candidate?.close(); } catch { /* acquisition already failed */ }
362
+ try { candidateFile?.close(); } catch { /* acquisition already failed */ }
363
+ if (mappedError instanceof NativeProfileError) throw mappedError;
364
+ if (!this.isLockBusy(mappedError)) {
365
+ throw this.profileLockUnavailable();
366
+ }
367
+ if (this.now() >= deadline) {
368
+ throw new NativeProfileError("NATIVE_PROFILE_BUSY", "Another native-profile operation is still running.", 503, true);
369
+ }
370
+ await this.sleep(50);
371
+ }
372
+ }
373
+ try {
374
+ this.activeLockFile = lockFile ?? null;
375
+ this.activeRootIdentity = await this.ensureRoot();
376
+ await this.onLockAcquired();
377
+ this.assertOperationStorageStable();
378
+ const result = await operation();
379
+ this.assertOperationStorageStable();
380
+ return result;
381
+ } finally {
382
+ try { database.exec("ROLLBACK"); } catch { /* close still releases the OS-backed lock */ }
383
+ try { database.close(); } catch { /* transaction is already ending */ }
384
+ try { lockFile?.close(); } catch { /* SQLite handle already released the transaction */ }
385
+ this.activeRootIdentity = null;
386
+ this.activeLockFile = null;
387
+ }
388
+ }
389
+
390
+ private assertOperationStorageStable(): void {
391
+ if (!this.activeRootIdentity || !this.activeLockFile) {
392
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "Native-profile storage ownership is not active.", 409);
393
+ }
394
+ this.assertStableProfileLockFile(this.activeLockFile);
395
+ assertPathIdentity(this.context.rootDir, this.activeRootIdentity, "The native-profile metadata root");
396
+ assertNativeProfileMetadataLayout(this.context);
397
+ }
398
+
399
+ private async assertNativeCodexStopped(confirmedStopped: boolean): Promise<void> {
400
+ const processState = await this.processProbe();
401
+ if (processState.status === "busy") {
402
+ throw new NativeProfileError("CODEX_BUSY", `Close the ${processState.count} detected native Codex process(es) before switching.`, 409);
403
+ }
404
+ if (processState.status === "unknown" && !confirmedStopped) {
405
+ throw new NativeProfileError("CODEX_PROCESS_CHECK_UNAVAILABLE", "Codex process state could not be confirmed; close Codex and retry with explicit confirmation.", 409);
406
+ }
407
+ }
408
+
409
+ private async keyForVault(vault: NativeMainProfileVaultV1 | null): Promise<NativeProfileKey> {
410
+ const existing = await this.keyProvider.get(this.context.homeId);
411
+ if (existing) return existing;
412
+ if (vault) {
413
+ throw new NativeProfileError("KEYRING_KEY_MISSING", "The OS credential-store key for this vault is missing.", 409);
414
+ }
415
+ return this.keyProvider.create(this.context.homeId);
416
+ }
417
+
418
+ private async writeVault(vault: NativeMainProfileVaultV1): Promise<void> {
419
+ this.assertOperationStorageStable();
420
+ await this.atomicWrite(this.context.vaultPath, serializeNativeProfileMetadata(vault));
421
+ this.assertOperationStorageStable();
422
+ }
423
+
424
+ private async writeJournal(journal: NativeProfileSwitchJournalV1): Promise<void> {
425
+ this.assertOperationStorageStable();
426
+ await this.atomicWrite(this.context.journalPath, serializeNativeProfileJournal(journal));
427
+ this.assertOperationStorageStable();
428
+ }
429
+
430
+ private removeJournal(): void {
431
+ this.assertOperationStorageStable();
432
+ try { unlinkSync(this.context.journalPath); } catch (error) { if (errorCode(error) !== "ENOENT") throw error; }
433
+ }
434
+
435
+ private removeRecoveryBlock(): void {
436
+ this.assertOperationStorageStable();
437
+ try { unlinkSync(this.context.recoveryBlockPath); } catch (error) { if (errorCode(error) !== "ENOENT") throw error; }
438
+ }
439
+
440
+ private async currentOwnershipConfirmed(): Promise<boolean> {
441
+ let key: NativeProfileKey | null = null;
442
+ let current: NativeEnvelopeSnapshot | null = null;
443
+ try {
444
+ const vault = this.readVault();
445
+ if (!vault) return false;
446
+ key = await this.keyForVault(vault);
447
+ current = this.readEnvelope(this.context.authPath);
448
+ this.assertCurrentIdentity(vault, current, key);
449
+ return true;
450
+ } catch {
451
+ return false;
452
+ } finally {
453
+ current?.raw.fill(0);
454
+ key?.key.fill(0);
455
+ }
456
+ }
457
+
458
+ private async clearRecoveryBlockLocked(): Promise<Record<string, unknown>> {
459
+ if (!(await this.currentOwnershipConfirmed())) {
460
+ throw new NativeProfileError(
461
+ "RECOVERY_REQUIRED",
462
+ "The quarantined recovery state remains blocked until auth.json matches the active encrypted profile.",
463
+ 409,
464
+ );
465
+ }
466
+ this.removeRecoveryBlock();
467
+ return {
468
+ ok: true,
469
+ recovered: true,
470
+ action: "confirm-current-owner",
471
+ externallyRefreshed: false,
472
+ effectiveCodexHome: this.context.codexHome,
473
+ restartRequired: false,
474
+ };
475
+ }
476
+
477
+ private async quarantineInvalidJournalLocked(): Promise<Record<string, unknown>> {
478
+ const quarantineFile = this.context.homeId + ".journal.quarantine-" + this.uuid() + ".json";
479
+ const quarantinePath = join(this.context.rootDir, quarantineFile);
480
+ this.assertOperationStorageStable();
481
+ await this.atomicWrite(this.context.recoveryBlockPath, serializeNativeProfileMetadata({
482
+ version: 1,
483
+ homeId: this.context.homeId,
484
+ reason: "malformed-journal",
485
+ quarantineFile,
486
+ createdAt: new Date(this.now()).toISOString(),
487
+ }));
488
+ this.assertOperationStorageStable();
489
+ try {
490
+ renameSync(this.context.journalPath, quarantinePath);
491
+ await this.hardenPath(quarantinePath);
492
+ this.assertOperationStorageStable();
493
+ } catch {
494
+ throw new NativeProfileError(
495
+ "RECOVERY_REQUIRED",
496
+ "The invalid recovery journal could not be quarantined; the fail-closed recovery marker remains active.",
497
+ 409,
498
+ );
499
+ }
500
+ if (!(await this.currentOwnershipConfirmed())) {
501
+ throw new NativeProfileError(
502
+ "RECOVERY_REQUIRED",
503
+ "The invalid recovery journal was quarantined as " + quarantineFile + ", but current credential ownership is not confirmed.",
504
+ 409,
505
+ );
506
+ }
507
+ this.removeRecoveryBlock();
508
+ return {
509
+ ok: true,
510
+ recovered: true,
511
+ action: "quarantine-invalid-journal",
512
+ quarantineFile,
513
+ externallyRefreshed: false,
514
+ effectiveCodexHome: this.context.codexHome,
515
+ restartRequired: false,
516
+ };
517
+ }
518
+
519
+ private assertNoPendingRecovery(): void {
520
+ if (probeNativeProfileRecoveryState(this.context) === "none") return;
521
+ throw new NativeProfileError(
522
+ "RECOVERY_REQUIRED",
523
+ "A native-profile recovery journal is pending. Run `ocx account main recover` or `ocx account main recover --rollback --yes` before registering or adding profiles.",
524
+ 409,
525
+ );
526
+ }
527
+
528
+ private requireVault(): NativeMainProfileVaultV1 {
529
+ const vault = this.readVault();
530
+ if (!vault) throw new NativeProfileError("PROFILE_NOT_FOUND", "Register the current native login before adding or switching profiles.", 404);
531
+ return vault;
532
+ }
533
+
534
+ private currentProfile(vault: NativeMainProfileVaultV1): NativeMainProfileRecordV1 {
535
+ const profile = vault.profiles.find(item => item.id === vault.activeProfileId && item.state === "active");
536
+ if (!profile) throw new NativeProfileError("VAULT_INVALID", "The native-profile vault has no active owner.", 409);
537
+ return profile;
538
+ }
539
+
540
+ private assertCurrentIdentity(vault: NativeMainProfileVaultV1, envelope: NativeEnvelopeSnapshot, key: NativeProfileKey): NativeMainProfileRecordV1 {
541
+ const active = this.currentProfile(vault);
542
+ if (nativeIdentityHash(key.key, envelope.accountId) !== active.identityHash) {
543
+ throw new NativeProfileError(
544
+ "ACTIVE_PROFILE_MISMATCH",
545
+ "The physical native login changed outside OpenCodex; recover or register the expected login before continuing.",
546
+ 409,
547
+ );
548
+ }
549
+ return active;
550
+ }
551
+
552
+ async register(labelInput: string): Promise<{ effectiveCodexHome: string; profile: NativeProfilePublic }> {
553
+ return this.withLock(async () => {
554
+ this.assertNoPendingRecovery();
555
+ requireFileCredentialStore(this.context);
556
+ const label = validateNativeProfileLabel(labelInput);
557
+ let envelope: NativeEnvelopeSnapshot | null = null;
558
+ let key: NativeProfileKey | null = null;
559
+ try {
560
+ envelope = this.readEnvelope(this.context.authPath);
561
+ let vault = this.readVault();
562
+ key = await this.keyForVault(vault);
563
+ const identityHash = nativeIdentityHash(key.key, envelope.accountId);
564
+ const timestamp = new Date(this.now()).toISOString();
565
+ if (!vault) {
566
+ const id = this.uuid();
567
+ assertUniqueNativeProfileLabel({ profiles: [] }, label, undefined, [id]);
568
+ vault = {
569
+ version: 1,
570
+ revision: 1,
571
+ homeId: this.context.homeId,
572
+ activeProfileId: id,
573
+ profiles: [{
574
+ id,
575
+ label,
576
+ identityHash,
577
+ identityHint: nativeIdentityHint(identityHash),
578
+ state: "active",
579
+ payload: null,
580
+ createdAt: timestamp,
581
+ updatedAt: timestamp,
582
+ }],
583
+ };
584
+ } else {
585
+ const active = this.assertCurrentIdentity(vault, envelope, key);
586
+ assertUniqueNativeProfileLabel(vault, label, active.id);
587
+ active.label = label;
588
+ active.updatedAt = timestamp;
589
+ vault.revision += 1;
590
+ }
591
+ await this.writeVault(vault);
592
+ return { effectiveCodexHome: this.context.codexHome, profile: publicNativeProfile(this.currentProfile(vault)) };
593
+ } finally {
594
+ envelope?.raw.fill(0);
595
+ key?.key.fill(0);
596
+ }
597
+ });
598
+ }
599
+
600
+ async list(): Promise<NativeProfileListResult> {
601
+ const vault = readNativeProfileVault(this.context);
602
+ return {
603
+ effectiveCodexHome: this.context.codexHome,
604
+ activeProfileId: vault?.activeProfileId ?? null,
605
+ profiles: vault?.profiles.map(publicNativeProfile) ?? [],
606
+ };
607
+ }
608
+
609
+ async doctor(): Promise<Record<string, unknown>> {
610
+ return this.withLock(async () => {
611
+ let stagingSweep: "ok" | "cleanup-required" | "unreadable" = "ok";
612
+ try {
613
+ const sweep = await this.sweepStagesLocked();
614
+ if (sweep.cleanupFailed > 0) stagingSweep = "cleanup-required";
615
+ } catch (error) {
616
+ stagingSweep = error instanceof NativeProfileError && error.code === "STAGING_CLEANUP_REQUIRED"
617
+ ? "cleanup-required"
618
+ : "unreadable";
619
+ }
620
+ const mode = (() => {
621
+ try { return resolveNativeCredentialStoreMode(this.context); } catch { return "unknown"; }
622
+ })();
623
+ const auth = this.readEnvelopeResult(this.context.authPath);
624
+ try {
625
+ let vault: NativeMainProfileVaultV1 | null = null;
626
+ let vaultStatus: "ok" | "missing" | "invalid" = "missing";
627
+ try {
628
+ vault = this.readVault();
629
+ vaultStatus = vault ? "ok" : "missing";
630
+ } catch {
631
+ vaultStatus = "invalid";
632
+ }
633
+ let keyStore: "available" | "missing-key" | "unavailable" = "available";
634
+ try {
635
+ const key = await this.keyProvider.get(this.context.homeId);
636
+ if (vaultStatus !== "missing" && !key) keyStore = "missing-key";
637
+ if (key) key.key.fill(0);
638
+ } catch {
639
+ keyStore = "unavailable";
640
+ }
641
+ let stagingCount: number | null = 0;
642
+ try {
643
+ stagingCount = this.stageStore.records().length;
644
+ if (existsSync(this.context.stagingRoot)) stagingCount += Array.from(new Bun.Glob("*").scanSync({ cwd: this.context.stagingRoot, onlyFiles: false })).filter(name => !UUID_RE.test(name) || !this.stageStore.recordForStage(name)).length;
645
+ } catch {
646
+ stagingSweep = "unreadable";
647
+ stagingCount = null;
648
+ }
649
+ return {
650
+ effectiveCodexHome: this.context.codexHome,
651
+ credentialStoreMode: mode,
652
+ supported: mode === "file",
653
+ authStatus: auth.status,
654
+ keyStore,
655
+ vaultStatus,
656
+ profileCount: vaultStatus === "invalid" ? null : vault?.profiles.length ?? 0,
657
+ activeProfileId: vault?.activeProfileId ?? null,
658
+ recoveryPending: probeNativeProfileRecoveryState(this.context) !== "none",
659
+ recoveryState: probeNativeProfileRecoveryState(this.context),
660
+ stagingSweep,
661
+ stagingCount,
662
+ };
663
+ } finally {
664
+ if (auth.status === "ok") auth.envelope.raw.fill(0);
665
+ }
666
+ });
667
+ }
668
+
669
+ private stagePath(stageId: string): string {
670
+ if (!UUID_RE.test(stageId)) throw new NativeProfileError("INVALID_REQUEST", "The staging identifier is invalid.", 400);
671
+ return join(this.context.stagingRoot, stageId);
672
+ }
673
+
674
+ private assertStagingLayout(requireRoot = false): void {
675
+ if (!existsSync(this.context.configDir)) {
676
+ if (requireRoot) throw new NativeProfileError("STAGING_NOT_FOUND", "The native-login staging root is missing.", 404);
677
+ return;
678
+ }
679
+ const configDir = assertSafeDirectory(this.context.configDir, "The OpenCodex configuration root");
680
+ const stagingBase = dirname(this.context.stagingRoot);
681
+ if (!samePath(stagingBase, join(configDir, "native-main-profile-staging"))) {
682
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The native-login staging root escaped OPENCODEX_HOME.", 409);
683
+ }
684
+ if (!existsSync(stagingBase)) {
685
+ if (requireRoot) throw new NativeProfileError("STAGING_NOT_FOUND", "The native-login staging root is missing.", 404);
686
+ return;
687
+ }
688
+ const canonicalBase = assertSafeDirectory(stagingBase, "The native-login staging namespace");
689
+ if (!samePath(dirname(this.context.stagingRoot), canonicalBase)) {
690
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The native-login staging root is not contained by OPENCODEX_HOME.", 409);
691
+ }
692
+ if (!existsSync(this.context.stagingRoot)) {
693
+ if (requireRoot) throw new NativeProfileError("STAGING_NOT_FOUND", "The native-login staging root is missing.", 404);
694
+ return;
695
+ }
696
+ assertSafeDirectory(this.context.stagingRoot, "The native-login staging root");
697
+ }
698
+
699
+ private async ensureStagingRoot(): Promise<void> {
700
+ if (!existsSync(this.context.configDir)) mkdirSync(this.context.configDir, { recursive: true, mode: 0o700 });
701
+ this.assertStagingLayout(false);
702
+ const stagingBase = dirname(this.context.stagingRoot);
703
+ if (!existsSync(stagingBase)) mkdirSync(stagingBase, { mode: 0o700 });
704
+ assertSafeDirectory(stagingBase, "The native-login staging namespace");
705
+ await this.hardenPath(stagingBase);
706
+ if (!existsSync(this.context.stagingRoot)) mkdirSync(this.context.stagingRoot, { mode: 0o700 });
707
+ assertSafeDirectory(this.context.stagingRoot, "The native-login staging root");
708
+ await this.hardenPath(this.context.stagingRoot);
709
+ this.assertStagingLayout(true);
710
+ }
711
+
712
+ private deleteStage(path: string, identity?: PathIdentity): void {
713
+ if (identity) assertPathIdentity(path, identity, "The native-login staging directory");
714
+ const authPath = join(path, "auth.json");
715
+ let authFd: number | undefined;
716
+ try {
717
+ const flags = process.platform === "win32"
718
+ ? fsConstants.O_RDWR
719
+ : fsConstants.O_RDWR | fsConstants.O_NOFOLLOW;
720
+ authFd = openSync(authPath, flags);
721
+ const opened = fstatSync(authFd, { bigint: true });
722
+ const pathEntry = lstatSync(authPath, { bigint: true });
723
+ if (
724
+ !opened.isFile()
725
+ || !pathEntry.isFile()
726
+ || pathEntry.isSymbolicLink()
727
+ || opened.dev !== pathEntry.dev
728
+ || opened.ino !== pathEntry.ino
729
+ || opened.nlink !== 1n
730
+ ) throw new NativeProfileError(
731
+ "STAGING_CLEANUP_REQUIRED",
732
+ "The staged credential file is not privately owned; plaintext may remain and requires manual cleanup.",
733
+ 500,
734
+ false,
735
+ true,
736
+ true,
737
+ );
738
+ unlinkSync(authPath);
739
+ const unlinked = fstatSync(authFd, { bigint: true });
740
+ if (unlinked.dev !== opened.dev || unlinked.ino !== opened.ino || unlinked.nlink !== 0n) {
741
+ throw new NativeProfileError(
742
+ "STAGING_CLEANUP_REQUIRED",
743
+ "The staged credential file could not be exclusively unlinked; plaintext may remain and requires manual cleanup.",
744
+ 500,
745
+ false,
746
+ true,
747
+ true,
748
+ );
749
+ }
750
+ ftruncateSync(authFd, 0);
751
+ } catch (error) {
752
+ if (error instanceof NativeProfileError) throw error;
753
+ if (errorCode(error) !== "ENOENT") {
754
+ throw new NativeProfileError(
755
+ "STAGING_CLEANUP_REQUIRED",
756
+ "The staged credential file could not be securely scrubbed; plaintext may remain and requires manual cleanup.",
757
+ 500,
758
+ false,
759
+ true,
760
+ true,
761
+ );
762
+ }
763
+ } finally {
764
+ try { if (authFd !== undefined) closeSync(authFd); } catch { /* removal below remains required */ }
765
+ }
766
+ if (identity) assertPathIdentity(path, identity, "The native-login staging directory");
767
+ this.removeStageTree(path);
768
+ }
769
+
770
+ private deleteLegacyStageById(stageId: string, requirePresent = false): void {
771
+ this.assertStagingLayout(true);
772
+ const expected = resolve(this.stagePath(stageId));
773
+ let stageStat: ReturnType<typeof lstatSync>;
774
+ try { stageStat = lstatSync(expected); } catch (error) {
775
+ if (errorCode(error) === "ENOENT") {
776
+ if (requirePresent) throw new NativeProfileError("STAGING_NOT_FOUND", "The native-login staging session is missing.", 404);
777
+ return;
778
+ }
779
+ throw error;
780
+ }
781
+ if (stageStat.isSymbolicLink() || !stageStat.isDirectory()) {
782
+ throw new NativeProfileError(
783
+ "PROFILE_STORAGE_UNSAFE",
784
+ "The native-login staging path is not a private directory.",
785
+ 409,
786
+ false,
787
+ true,
788
+ true,
789
+ );
790
+ }
791
+ const canonicalRoot = resolve(realpathSync.native(this.context.stagingRoot));
792
+ const canonicalExpected = resolve(realpathSync.native(expected));
793
+ const rel = relative(canonicalRoot, canonicalExpected);
794
+ if (!rel || rel.startsWith("..") || rel.includes(sep)) {
795
+ throw new NativeProfileError("STAGING_CLEANUP_REQUIRED", "The staging path could not be safely removed.", 500, false, true, true);
796
+ }
797
+ this.deleteStage(canonicalExpected, pathIdentity(canonicalExpected));
798
+ }
799
+
800
+ private async cleanupRegisteredStage(
801
+ proof: NativeStageProof,
802
+ outcome: Exclude<NativeStageTerminalOutcome, null>,
803
+ ): Promise<NativeStageCleanupResult> {
804
+ let stageRemoved = false;
805
+ let plaintextMayRemain = true;
806
+ try {
807
+ this.deleteStage(proof.path, proof.identity);
808
+ stageRemoved = true;
809
+ plaintextMayRemain = false;
810
+ } catch (error) {
811
+ plaintextMayRemain = error instanceof NativeProfileError
812
+ ? error.plaintextMayRemain !== false
813
+ : false;
814
+ }
815
+ if (stageRemoved) {
816
+ try {
817
+ await this.stageStore.remove(proof);
818
+ return { removed: true, plaintextMayRemain: false };
819
+ } catch {
820
+ try { await this.stageStore.markCleanupRequired(proof, outcome); } catch { /* the cleanup result remains authoritative */ }
821
+ return { removed: false, plaintextMayRemain: false };
822
+ }
823
+ }
824
+ try { await this.stageStore.markCleanupRequired(proof, outcome); } catch { /* preserve the plaintext signal */ }
825
+ return { removed: false, plaintextMayRemain };
826
+ }
827
+
828
+ private async sweepStagesLocked(): Promise<NativeStageSweepResult> {
829
+ let removed = 0;
830
+ let live = 0;
831
+ let cleanupFailed = 0;
832
+ let plaintextMayRemain = false;
833
+ const records = this.stageStore.records();
834
+ const registeredHere = new Set(
835
+ records
836
+ .filter(record => samePath(record.stagingRoot, this.context.stagingRoot))
837
+ .map(record => record.stageId),
838
+ );
839
+
840
+ for (const record of records) {
841
+ if (record.state === "open" && this.now() <= record.leaseExpiresAt) {
842
+ live += 1;
843
+ continue;
844
+ }
845
+ const outcome = record.terminalOutcome
846
+ ?? (record.state === "creating" ? "cancelled" : "expired");
847
+ let proof: NativeStageProof | null;
848
+ try {
849
+ proof = this.stageStore.proofForRecord(record, record.state === "creating");
850
+ } catch {
851
+ cleanupFailed += 1;
852
+ plaintextMayRemain = true;
853
+ try {
854
+ await this.stageStore.markCleanupRequired({ record }, outcome);
855
+ } catch { /* fail closed and report the unresolved plaintext boundary */ }
856
+ continue;
857
+ }
858
+ if (!proof) {
859
+ try {
860
+ await this.stageStore.remove({ record });
861
+ removed += 1;
862
+ } catch {
863
+ cleanupFailed += 1;
864
+ }
865
+ continue;
866
+ }
867
+ const cleanup = await this.cleanupRegisteredStage(proof, outcome);
868
+ if (cleanup.removed) removed += 1;
869
+ else cleanupFailed += 1;
870
+ plaintextMayRemain ||= cleanup.plaintextMayRemain;
871
+ }
872
+
873
+ if (existsSync(this.context.stagingRoot)) {
874
+ this.assertStagingLayout(true);
875
+ for (const name of readdirSync(this.context.stagingRoot)) {
876
+ if (!UUID_RE.test(name) || registeredHere.has(name)) continue;
877
+ const path = this.stagePath(name);
878
+ let createdAt: number;
879
+ try {
880
+ const entry = lstatSync(path);
881
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
882
+ cleanupFailed += 1;
883
+ plaintextMayRemain = true;
884
+ continue;
885
+ }
886
+ createdAt = entry.mtimeMs;
887
+ try {
888
+ const metadata = JSON.parse(readFileSync(join(path, "stage.json"), "utf8")) as Record<string, unknown>;
889
+ if (typeof metadata.createdAt === "number") createdAt = metadata.createdAt;
890
+ } catch { /* mtime bounds legacy crash residue */ }
891
+ } catch (error) {
892
+ if (errorCode(error) === "ENOENT") continue;
893
+ cleanupFailed += 1;
894
+ plaintextMayRemain = true;
895
+ continue;
896
+ }
897
+ if (this.now() - createdAt <= LEGACY_STAGE_MAX_AGE_MS) {
898
+ live += 1;
899
+ continue;
900
+ }
901
+ try {
902
+ this.deleteLegacyStageById(name);
903
+ removed += 1;
904
+ } catch (error) {
905
+ cleanupFailed += 1;
906
+ plaintextMayRemain ||= error instanceof NativeProfileError
907
+ ? error.plaintextMayRemain !== false
908
+ : true;
909
+ }
910
+ }
911
+ }
912
+
913
+ return { removed, live, cleanupFailed, plaintextMayRemain };
914
+ }
915
+
916
+ /**
917
+ * Whether startup or the periodic cleaner has any stage state to inspect.
918
+ *
919
+ * Absence is the only lock-free result. Any entry or observation failure
920
+ * keeps the existing fail-closed sweep, so an unsafe/unreadable stage path
921
+ * can never be mistaken for an unused profile subsystem.
922
+ */
923
+ stageSweepRequired(): boolean {
924
+ for (const path of [this.context.stageRegistryPath, this.context.stagingRoot]) {
925
+ try {
926
+ lstatSync(path);
927
+ return true;
928
+ } catch (error) {
929
+ if (errorCode(error) !== "ENOENT") return true;
930
+ }
931
+ }
932
+ return false;
933
+ }
934
+
935
+ async sweepStages(): Promise<NativeStageSweepResult> {
936
+ return this.withLock(() => this.sweepStagesLocked());
937
+ }
938
+
939
+ async prepareStage(): Promise<{
940
+ stageId: string;
941
+ writerToken: string;
942
+ stagingCodexHome: string;
943
+ effectiveCodexHome: string;
944
+ leaseExpiresAt: number;
945
+ heartbeatIntervalMs: number;
946
+ }> {
947
+ return this.withLock(async () => {
948
+ this.assertNoPendingRecovery();
949
+ const sweep = await this.sweepStagesLocked();
950
+ if (sweep.plaintextMayRemain) {
951
+ throw new NativeProfileError(
952
+ "STAGING_CLEANUP_REQUIRED",
953
+ "An expired native-login staging session could not be securely removed.",
954
+ 500,
955
+ true,
956
+ true,
957
+ true,
958
+ );
959
+ }
960
+ requireFileCredentialStore(this.context);
961
+ const vault = this.requireVault();
962
+ let key: NativeProfileKey | null = null;
963
+ let current: NativeEnvelopeSnapshot | null = null;
964
+ let reservation: Awaited<ReturnType<NativeProfileStageStore["reserve"]>> | null = null;
965
+ try {
966
+ key = await this.keyForVault(vault);
967
+ current = this.readEnvelope(this.context.authPath);
968
+ this.assertCurrentIdentity(vault, current, key);
969
+ await this.ensureStagingRoot();
970
+ reservation = await this.stageStore.reserve(this.context.stagingRoot);
971
+ mkdirSync(reservation.stagingCodexHome, { mode: 0o700 });
972
+ await this.hardenPath(reservation.stagingCodexHome);
973
+ writeFileSync(join(reservation.stagingCodexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', { mode: 0o600 });
974
+ await this.hardenPath(join(reservation.stagingCodexHome, "config.toml"));
975
+ writeFileSync(join(reservation.stagingCodexHome, "stage.json"), `${JSON.stringify({
976
+ version: 2,
977
+ stageId: reservation.stageId,
978
+ leaseId: reservation.leaseId,
979
+ homeId: this.context.homeId,
980
+ createdAt: this.now(),
981
+ }, null, 2)}\n`, { mode: 0o600 });
982
+ await this.hardenPath(join(reservation.stagingCodexHome, "stage.json"));
983
+ await this.stageStore.activate(reservation);
984
+ return {
985
+ stageId: reservation.stageId,
986
+ writerToken: reservation.writerToken,
987
+ stagingCodexHome: reservation.stagingCodexHome,
988
+ effectiveCodexHome: this.context.codexHome,
989
+ leaseExpiresAt: reservation.leaseExpiresAt,
990
+ heartbeatIntervalMs: NATIVE_STAGE_HEARTBEAT_INTERVAL_MS,
991
+ };
992
+ } catch (error) {
993
+ let plaintextMayRemain = false;
994
+ if (reservation) {
995
+ const record = this.stageStore.recordForStage(reservation.stageId);
996
+ if (record) {
997
+ try {
998
+ const proof = this.stageStore.proofForRecord(record, true);
999
+ if (proof) {
1000
+ const cleanup = await this.cleanupRegisteredStage(proof, "cancelled");
1001
+ plaintextMayRemain = cleanup.plaintextMayRemain;
1002
+ } else {
1003
+ await this.stageStore.remove({ record });
1004
+ }
1005
+ } catch {
1006
+ plaintextMayRemain = true;
1007
+ }
1008
+ }
1009
+ }
1010
+ if (error instanceof NativeProfileError) {
1011
+ throw new NativeProfileError(
1012
+ error.code,
1013
+ error.message,
1014
+ error.status,
1015
+ error.retryable,
1016
+ plaintextMayRemain || error.cleanupRequired === true ? true : undefined,
1017
+ plaintextMayRemain || error.plaintextMayRemain === true,
1018
+ );
1019
+ }
1020
+ if (plaintextMayRemain) {
1021
+ throw new NativeProfileError(
1022
+ "STAGING_CLEANUP_REQUIRED",
1023
+ "Native-login staging failed and plaintext may remain.",
1024
+ 500,
1025
+ false,
1026
+ true,
1027
+ true,
1028
+ );
1029
+ }
1030
+ throw error;
1031
+ } finally {
1032
+ current?.raw.fill(0);
1033
+ key?.key.fill(0);
1034
+ }
1035
+ });
1036
+ }
1037
+
1038
+ async heartbeatStage(stageId: string, writerToken: string): Promise<{ ok: true; leaseExpiresAt: number }> {
1039
+ return this.withLock(async () => {
1040
+ const proof = this.stageStore.verify(stageId, writerToken);
1041
+ if (proof.expired) {
1042
+ const cleanup = await this.cleanupRegisteredStage(proof, "expired");
1043
+ throw new NativeProfileError(
1044
+ "STAGING_EXPIRED",
1045
+ "The native-login staging lease expired.",
1046
+ 410,
1047
+ false,
1048
+ cleanup.removed ? undefined : true,
1049
+ cleanup.plaintextMayRemain,
1050
+ );
1051
+ }
1052
+ const leaseExpiresAt = await this.stageStore.heartbeat(proof);
1053
+ return { ok: true, leaseExpiresAt };
1054
+ });
1055
+ }
1056
+
1057
+ async finishStage(
1058
+ stageId: string,
1059
+ writerToken: string,
1060
+ labelInput: string,
1061
+ ): Promise<{ effectiveCodexHome: string; profile: NativeProfilePublic; plaintextMayRemain: false }> {
1062
+ return this.withLock(async () => {
1063
+ const ownedStage = this.stageStore.verify(stageId, writerToken);
1064
+ if (ownedStage.expired) {
1065
+ const cleanup = await this.cleanupRegisteredStage(ownedStage, "expired");
1066
+ throw new NativeProfileError(
1067
+ "STAGING_EXPIRED",
1068
+ "The native-login staging lease expired.",
1069
+ 410,
1070
+ false,
1071
+ cleanup.removed ? undefined : true,
1072
+ cleanup.plaintextMayRemain,
1073
+ );
1074
+ }
1075
+ let target: NativeEnvelopeSnapshot | null = null;
1076
+ let current: NativeEnvelopeSnapshot | null = null;
1077
+ let key: NativeProfileKey | null = null;
1078
+ let operationFailed = false;
1079
+ let operationError: unknown;
1080
+ let importCommitted = false;
1081
+ let result: { effectiveCodexHome: string; profile: NativeProfilePublic; plaintextMayRemain: false } | undefined;
1082
+ try {
1083
+ this.assertNoPendingRecovery();
1084
+ requireFileCredentialStore(this.context);
1085
+ const label = validateNativeProfileLabel(labelInput);
1086
+ target = readNativeEnvelope(join(ownedStage.path, "auth.json"));
1087
+ assertPathIdentity(ownedStage.path, ownedStage.identity, "The native-login staging directory");
1088
+ current = readNativeEnvelope(this.context.authPath);
1089
+ const vault = this.requireVault();
1090
+ if (vault.profiles.length >= MAX_NATIVE_PROFILES) {
1091
+ throw new NativeProfileError("INVALID_REQUEST", "Native profiles are limited to 32 entries.", 400);
1092
+ }
1093
+ key = await this.keyForVault(vault);
1094
+ this.assertCurrentIdentity(vault, current, key);
1095
+ const id = this.uuid();
1096
+ assertUniqueNativeProfileLabel(vault, label, undefined, [id]);
1097
+ const identityHash = nativeIdentityHash(key.key, target.accountId);
1098
+ if (vault.profiles.some(profile => profile.identityHash === identityHash)) {
1099
+ throw new NativeProfileError("PROFILE_ALREADY_EXISTS", "That native identity is already registered.", 409);
1100
+ }
1101
+ const timestamp = new Date(this.now()).toISOString();
1102
+ const currentProfile = this.currentProfile(vault);
1103
+ const currentSourcePayload = encryptNativeEnvelope(
1104
+ this.context,
1105
+ currentProfile.id,
1106
+ currentProfile.identityHash,
1107
+ current,
1108
+ key,
1109
+ );
1110
+ const profile: NativeMainProfileRecordV1 = {
1111
+ id,
1112
+ label,
1113
+ identityHash,
1114
+ identityHint: nativeIdentityHint(identityHash),
1115
+ state: "inactive",
1116
+ payload: encryptNativeEnvelope(this.context, id, identityHash, target, key),
1117
+ createdAt: timestamp,
1118
+ updatedAt: timestamp,
1119
+ };
1120
+ const prospectiveVault = structuredClone(vault);
1121
+ prospectiveVault.profiles.push(profile);
1122
+ prospectiveVault.revision += 1;
1123
+ const currentActivePreflight = structuredClone(prospectiveVault);
1124
+ currentActivePreflight.revision = Number.MAX_SAFE_INTEGER;
1125
+ serializeNativeProfileMetadata(currentActivePreflight);
1126
+ for (const inactiveProfile of prospectiveVault.profiles.filter(item => item.state === "inactive")) {
1127
+ const activePlacement = structuredClone(prospectiveVault);
1128
+ activePlacement.revision = Number.MAX_SAFE_INTEGER;
1129
+ const nextCurrent = activePlacement.profiles.find(item => item.id === currentProfile.id)!;
1130
+ const nextActive = activePlacement.profiles.find(item => item.id === inactiveProfile.id)!;
1131
+ nextCurrent.state = "inactive";
1132
+ nextCurrent.payload = currentSourcePayload;
1133
+ nextActive.state = "active";
1134
+ nextActive.payload = null;
1135
+ activePlacement.activeProfileId = nextActive.id;
1136
+ serializeNativeProfileMetadata(activePlacement);
1137
+ }
1138
+ await this.writeVault(prospectiveVault);
1139
+ importCommitted = true;
1140
+ result = {
1141
+ effectiveCodexHome: this.context.codexHome,
1142
+ profile: publicNativeProfile(profile),
1143
+ plaintextMayRemain: false,
1144
+ };
1145
+ } catch (error) {
1146
+ operationFailed = true;
1147
+ operationError = error;
1148
+ } finally {
1149
+ target?.raw.fill(0);
1150
+ current?.raw.fill(0);
1151
+ key?.key.fill(0);
1152
+ }
1153
+
1154
+ const cleanup = await this.cleanupRegisteredStage(
1155
+ ownedStage,
1156
+ importCommitted ? "imported" : "rejected",
1157
+ );
1158
+ if (operationFailed) {
1159
+ if (operationError instanceof NativeProfileError) {
1160
+ throw new NativeProfileError(
1161
+ operationError.code,
1162
+ operationError.message,
1163
+ operationError.status,
1164
+ operationError.retryable,
1165
+ cleanup.removed ? operationError.cleanupRequired : true,
1166
+ cleanup.plaintextMayRemain,
1167
+ );
1168
+ }
1169
+ throw new NativeProfileError(
1170
+ "INTERNAL_ERROR",
1171
+ cleanup.removed
1172
+ ? "The native profile import failed."
1173
+ : "The native profile import failed and its staging session could not be securely removed.",
1174
+ 500,
1175
+ false,
1176
+ cleanup.removed ? undefined : true,
1177
+ cleanup.plaintextMayRemain,
1178
+ );
1179
+ }
1180
+ if (!cleanup.removed) {
1181
+ throw new NativeProfileError(
1182
+ "STAGING_CLEANUP_REQUIRED",
1183
+ importCommitted
1184
+ ? "The native profile was imported, but its staging session could not be securely removed. Do not retry the import; cancel it explicitly."
1185
+ : "The native-login staging session could not be securely removed; cancel it explicitly.",
1186
+ 500,
1187
+ false,
1188
+ importCommitted ? undefined : true,
1189
+ cleanup.plaintextMayRemain,
1190
+ );
1191
+ }
1192
+ return result!;
1193
+ });
1194
+ }
1195
+
1196
+ async cancelStage(stageId: string, writerToken: string): Promise<NativeStageCleanupResult> {
1197
+ return this.withLock(async () => {
1198
+ const proof = this.stageStore.verify(stageId, writerToken, true, true);
1199
+ const outcome = proof.record.terminalOutcome ?? "cancelled";
1200
+ const cleanup = await this.cleanupRegisteredStage(proof, outcome);
1201
+ if (!cleanup.removed) {
1202
+ throw new NativeProfileError(
1203
+ "STAGING_CLEANUP_REQUIRED",
1204
+ "The native-login staging session could not be securely removed.",
1205
+ 500,
1206
+ true,
1207
+ true,
1208
+ cleanup.plaintextMayRemain,
1209
+ );
1210
+ }
1211
+ return cleanup;
1212
+ });
1213
+ }
1214
+ private resolveTarget(vault: NativeMainProfileVaultV1, target: string): NativeMainProfileRecordV1 {
1215
+ const selectors = new Map<string, NativeMainProfileRecordV1>();
1216
+ for (const item of vault.profiles) {
1217
+ for (const selector of [item.id, item.label]) {
1218
+ const key = nativeProfileSelectorKey(selector);
1219
+ if (selectors.has(key)) {
1220
+ throw new NativeProfileError(
1221
+ "VAULT_INVALID",
1222
+ "The native-profile vault contains an ambiguous ID or label selector.",
1223
+ 409,
1224
+ );
1225
+ }
1226
+ selectors.set(key, item);
1227
+ }
1228
+ }
1229
+ const profile = selectors.get(nativeProfileSelectorKey(target));
1230
+ if (!profile) throw new NativeProfileError("PROFILE_NOT_FOUND", "The requested native profile does not exist.", 404);
1231
+ if (profile.state !== "inactive" || !profile.payload) {
1232
+ throw new NativeProfileError("INVALID_REQUEST", "The requested native profile is already active.", 400);
1233
+ }
1234
+ return profile;
1235
+ }
1236
+
1237
+ private verifyWrittenEnvelope(expectedDigest: string, expectedIdentityHash: string, key: NativeProfileKey): NativeEnvelopeSnapshot {
1238
+ const observed = this.readEnvelope(this.context.authPath);
1239
+ try {
1240
+ if (observed.digest !== expectedDigest || nativeIdentityHash(key.key, observed.accountId) !== expectedIdentityHash) {
1241
+ throw new Error("auth read-back mismatch");
1242
+ }
1243
+ return observed;
1244
+ } catch (error) {
1245
+ observed.raw.fill(0);
1246
+ throw error;
1247
+ }
1248
+ }
1249
+
1250
+ async switch(targetSelector: string, confirmedStopped = false): Promise<Record<string, unknown>> {
1251
+ return this.withLock(async () => {
1252
+ await this.sweepStagesLocked();
1253
+ requireFileCredentialStore(this.context);
1254
+ await this.assertNativeCodexStopped(confirmedStopped);
1255
+ if (probeNativeProfileRecoveryState(this.context) !== "none") await this.recoverLocked(false);
1256
+ const beforeVault = this.requireVault();
1257
+ let key: NativeProfileKey | null = null;
1258
+ let source: NativeEnvelopeSnapshot | null = null;
1259
+ let target: NativeEnvelopeSnapshot | null = null;
1260
+ let journalPrepared = false;
1261
+ let committed = false;
1262
+ try {
1263
+ key = await this.keyForVault(beforeVault);
1264
+ source = this.readEnvelope(this.context.authPath);
1265
+ const sourceProfile = this.assertCurrentIdentity(beforeVault, source, key);
1266
+ const targetProfile = this.resolveTarget(beforeVault, targetSelector);
1267
+ target = decryptNativeEnvelope(this.context, targetProfile.id, targetProfile.identityHash, targetProfile.payload!, key);
1268
+ if (nativeIdentityHash(key.key, target.accountId) !== targetProfile.identityHash) {
1269
+ throw new NativeProfileError("PROFILE_DECRYPT_FAILED", "The selected native profile identity does not match its encrypted record.", 409);
1270
+ }
1271
+ const timestamp = new Date(this.now()).toISOString();
1272
+ const sourcePayload = encryptNativeEnvelope(this.context, sourceProfile.id, sourceProfile.identityHash, source, key);
1273
+ const afterVault = structuredClone(beforeVault);
1274
+ const nextSource = afterVault.profiles.find(profile => profile.id === sourceProfile.id)!;
1275
+ const nextTarget = afterVault.profiles.find(profile => profile.id === targetProfile.id)!;
1276
+ nextSource.state = "inactive";
1277
+ nextSource.payload = sourcePayload;
1278
+ nextSource.updatedAt = timestamp;
1279
+ nextTarget.state = "active";
1280
+ nextTarget.payload = null;
1281
+ nextTarget.updatedAt = timestamp;
1282
+ afterVault.activeProfileId = nextTarget.id;
1283
+ afterVault.revision += 1;
1284
+ const journal: NativeProfileSwitchJournalV1 = {
1285
+ version: 1,
1286
+ transactionId: this.uuid(),
1287
+ homeId: this.context.homeId,
1288
+ phase: "prepared",
1289
+ sourceProfileId: sourceProfile.id,
1290
+ sourceIdentityHash: sourceProfile.identityHash,
1291
+ sourcePayload,
1292
+ targetProfileId: targetProfile.id,
1293
+ targetIdentityHash: targetProfile.identityHash,
1294
+ targetPayload: targetProfile.payload!,
1295
+ beforeVault: structuredClone(beforeVault),
1296
+ afterVault,
1297
+ createdAt: timestamp,
1298
+ };
1299
+ serializeNativeProfileMetadata(afterVault);
1300
+ serializeNativeProfileJournal({ ...journal, phase: "prepared" });
1301
+ serializeNativeProfileJournal({ ...journal, phase: "auth-replaced" });
1302
+ serializeNativeProfileJournal({ ...journal, phase: "vault-committed" });
1303
+ await this.writeJournal(journal);
1304
+ journalPrepared = true;
1305
+ await this.onSwitchBoundary("journal-prepared");
1306
+ this.assertOperationStorageStable();
1307
+ await this.atomicWrite(this.context.authPath, target.text);
1308
+ const observedTarget = this.verifyWrittenEnvelope(target.digest, targetProfile.identityHash, key);
1309
+ observedTarget.raw.fill(0);
1310
+ await this.onSwitchBoundary("auth-replaced");
1311
+ journal.phase = "auth-replaced";
1312
+ await this.writeJournal(journal);
1313
+ await this.writeVault(afterVault);
1314
+ committed = true;
1315
+ await this.onSwitchBoundary("vault-committed");
1316
+ journal.phase = "vault-committed";
1317
+ await this.writeJournal(journal);
1318
+ this.applyTransition(source.accountId, target.accountId);
1319
+ await this.onSwitchBoundary("runtime-transition-published");
1320
+ this.removeJournal();
1321
+ await this.onSwitchBoundary("journal-deleted");
1322
+ return {
1323
+ ok: true,
1324
+ effectiveCodexHome: this.context.codexHome,
1325
+ activeProfile: publicNativeProfile(nextTarget),
1326
+ restartRequired: true,
1327
+ };
1328
+ } catch (cause) {
1329
+ if (!journalPrepared) throw cause;
1330
+ if (committed) {
1331
+ throw new NativeProfileError("RECOVERY_REQUIRED", "The login changed, but final transaction cleanup requires recovery.", 409);
1332
+ }
1333
+ try {
1334
+ this.assertOperationStorageStable();
1335
+ await this.atomicWrite(this.context.authPath, source!.text);
1336
+ const restored = this.verifyWrittenEnvelope(source!.digest, nativeIdentityHash(key!.key, source!.accountId), key!);
1337
+ restored.raw.fill(0);
1338
+ await this.writeVault(beforeVault);
1339
+ this.removeJournal();
1340
+ } catch {
1341
+ throw new NativeProfileError(
1342
+ "AUTH_RESTORE_FAILED",
1343
+ "The original native login could not be verified after rollback; the encrypted recovery journal was retained.",
1344
+ 500,
1345
+ );
1346
+ }
1347
+ throw new NativeProfileError("SWITCH_ROLLED_BACK", "The native-login switch failed and the exact original login was restored.", 409);
1348
+ } finally {
1349
+ source?.raw.fill(0);
1350
+ target?.raw.fill(0);
1351
+ key?.key.fill(0);
1352
+ }
1353
+ });
1354
+ }
1355
+
1356
+ private async recoverLocked(rollback: boolean): Promise<Record<string, unknown>> {
1357
+ const inspection = inspectNativeProfileJournal(this.context);
1358
+ const recoveryState = probeNativeProfileRecoveryState(this.context);
1359
+ if (inspection.status === "invalid" || recoveryState === "unreadable") {
1360
+ throw new NativeProfileError(
1361
+ "RECOVERY_REQUIRED",
1362
+ "The native-profile recovery state is invalid and requires explicit confirmed rollback quarantine.",
1363
+ 409,
1364
+ );
1365
+ }
1366
+ if (recoveryState === "manual") {
1367
+ if (inspection.status !== "missing") {
1368
+ throw new NativeProfileError("RECOVERY_REQUIRED", "Manual native-profile recovery remains pending.", 409);
1369
+ }
1370
+ return this.clearRecoveryBlockLocked();
1371
+ }
1372
+ const journal = inspection.status === "valid" ? inspection.journal : null;
1373
+ if (!journal) {
1374
+ return {
1375
+ ok: true,
1376
+ recovered: false,
1377
+ externallyRefreshed: false,
1378
+ effectiveCodexHome: this.context.codexHome,
1379
+ };
1380
+ }
1381
+ let key: NativeProfileKey | null = null;
1382
+ let current: ReturnType<typeof readNativeEnvelopeResult> | null = null;
1383
+ let sourceEnvelope: NativeEnvelopeSnapshot | null = null;
1384
+ try {
1385
+ key = await this.keyForVault(journal.beforeVault);
1386
+ current = readNativeEnvelopeResult(this.context.authPath);
1387
+ if (current.status !== "ok") {
1388
+ throw new NativeProfileError("RECOVERY_REQUIRED", "Recovery stopped because the current native login is not readable and confirmed.", 409);
1389
+ }
1390
+ const currentHash = nativeIdentityHash(key.key, current.envelope.accountId);
1391
+ if (rollback) {
1392
+ if (currentHash === journal.sourceIdentityHash) {
1393
+ await this.writeVault(journal.beforeVault);
1394
+ this.removeJournal();
1395
+ return {
1396
+ ok: true,
1397
+ recovered: true,
1398
+ action: "rollback-source",
1399
+ externallyRefreshed: current.envelope.digest !== journal.sourcePayload.envelopeSha256,
1400
+ effectiveCodexHome: this.context.codexHome,
1401
+ restartRequired: false,
1402
+ };
1403
+ }
1404
+ if (currentHash !== journal.targetIdentityHash) {
1405
+ throw new NativeProfileError("RECOVERY_REQUIRED", "Recovery found a third or unknown native identity and made no credential write.", 409);
1406
+ }
1407
+ sourceEnvelope = decryptNativeEnvelope(
1408
+ this.context,
1409
+ journal.sourceProfileId,
1410
+ journal.sourceIdentityHash,
1411
+ journal.sourcePayload,
1412
+ key,
1413
+ );
1414
+ let rollbackVault = journal.beforeVault;
1415
+ const externallyRefreshed = current.envelope.digest !== journal.targetPayload.envelopeSha256;
1416
+ let rollbackVaultPublished = false;
1417
+ if (current.envelope.digest !== journal.targetPayload.envelopeSha256) {
1418
+ rollbackVault = structuredClone(journal.beforeVault);
1419
+ const targetProfile = rollbackVault.profiles.find(profile =>
1420
+ profile.id === journal.targetProfileId
1421
+ && profile.identityHash === journal.targetIdentityHash
1422
+ && profile.state === "inactive"
1423
+ );
1424
+ if (!targetProfile) {
1425
+ throw new NativeProfileError(
1426
+ "VAULT_INVALID",
1427
+ "Recovery could not preserve the refreshed target login in its inactive profile and made no credential write.",
1428
+ 409,
1429
+ );
1430
+ }
1431
+ const refreshedTargetPayload = encryptNativeEnvelope(
1432
+ this.context,
1433
+ targetProfile.id,
1434
+ targetProfile.identityHash,
1435
+ current.envelope,
1436
+ key,
1437
+ );
1438
+ targetProfile.payload = refreshedTargetPayload;
1439
+ targetProfile.updatedAt = new Date(this.now()).toISOString();
1440
+ rollbackVault.revision += 1;
1441
+ const preservedJournal = structuredClone(journal);
1442
+ preservedJournal.targetPayload = refreshedTargetPayload;
1443
+ preservedJournal.beforeVault = structuredClone(rollbackVault);
1444
+ await this.writeJournal(preservedJournal);
1445
+ await this.writeVault(rollbackVault);
1446
+ rollbackVaultPublished = true;
1447
+ }
1448
+ this.assertOperationStorageStable();
1449
+ await this.atomicWrite(this.context.authPath, sourceEnvelope.text);
1450
+ const restored = this.verifyWrittenEnvelope(sourceEnvelope.digest, journal.sourceIdentityHash, key);
1451
+ restored.raw.fill(0);
1452
+ if (!rollbackVaultPublished) await this.writeVault(rollbackVault);
1453
+ this.applyTransition(current.envelope.accountId, sourceEnvelope.accountId);
1454
+ this.removeJournal();
1455
+ return {
1456
+ ok: true,
1457
+ recovered: true,
1458
+ action: "rollback-source",
1459
+ externallyRefreshed,
1460
+ effectiveCodexHome: this.context.codexHome,
1461
+ restartRequired: true,
1462
+ };
1463
+ }
1464
+ const observation = currentHash === journal.sourceIdentityHash
1465
+ ? { identity: "source" as const, digest: current.envelope.digest === journal.sourcePayload.envelopeSha256 ? "exact" as const : "changed" as const }
1466
+ : currentHash === journal.targetIdentityHash
1467
+ ? { identity: "target" as const, digest: current.envelope.digest === journal.targetPayload.envelopeSha256 ? "exact" as const : "changed" as const }
1468
+ : { identity: "other" as const, digest: "unknown" as const };
1469
+ const decision = decideNativeProfileRecovery(journal.phase, observation);
1470
+ if (decision.action === "manual-recovery") {
1471
+ throw new NativeProfileError("RECOVERY_REQUIRED", "Recovery found a third or unknown native identity and made no credential write.", 409);
1472
+ }
1473
+ if (decision.action === "rollback-source") {
1474
+ await this.writeVault(journal.beforeVault);
1475
+ this.removeJournal();
1476
+ return {
1477
+ ok: true,
1478
+ recovered: true,
1479
+ action: decision.action,
1480
+ externallyRefreshed: decision.externallyRefreshed,
1481
+ effectiveCodexHome: this.context.codexHome,
1482
+ restartRequired: false,
1483
+ };
1484
+ }
1485
+ sourceEnvelope = decryptNativeEnvelope(
1486
+ this.context,
1487
+ journal.sourceProfileId,
1488
+ journal.sourceIdentityHash,
1489
+ journal.sourcePayload,
1490
+ key,
1491
+ );
1492
+ await this.writeVault(journal.afterVault);
1493
+ this.applyTransition(sourceEnvelope.accountId, current.envelope.accountId);
1494
+ this.removeJournal();
1495
+ return {
1496
+ ok: true,
1497
+ recovered: true,
1498
+ action: decision.action,
1499
+ externallyRefreshed: decision.externallyRefreshed,
1500
+ effectiveCodexHome: this.context.codexHome,
1501
+ restartRequired: true,
1502
+ };
1503
+ } finally {
1504
+ if (current?.status === "ok") current.envelope.raw.fill(0);
1505
+ sourceEnvelope?.raw.fill(0);
1506
+ key?.key.fill(0);
1507
+ }
1508
+ }
1509
+
1510
+ async recover(rollback = false, confirmedStopped = false): Promise<Record<string, unknown>> {
1511
+ return this.withLock(async () => {
1512
+ requireFileCredentialStore(this.context);
1513
+ const inspection = inspectNativeProfileJournal(this.context);
1514
+ if (inspection.status === "invalid") {
1515
+ if (!rollback) {
1516
+ throw new NativeProfileError(
1517
+ "RECOVERY_REQUIRED",
1518
+ "The invalid native-profile recovery journal requires recover --rollback --yes.",
1519
+ 409,
1520
+ );
1521
+ }
1522
+ await this.assertNativeCodexStopped(confirmedStopped);
1523
+ return this.quarantineInvalidJournalLocked();
1524
+ }
1525
+ if (rollback && inspection.status === "valid") {
1526
+ await this.assertNativeCodexStopped(confirmedStopped);
1527
+ }
1528
+ return this.recoverLocked(rollback);
1529
+ });
1530
+ }
1531
+ }