@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -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 +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -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 +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,855 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHash,
5
+ createHmac,
6
+ randomBytes,
7
+ timingSafeEqual,
8
+ } from "node:crypto";
9
+ import {
10
+ closeSync,
11
+ constants as fsConstants,
12
+ existsSync,
13
+ fstatSync,
14
+ lstatSync,
15
+ openSync,
16
+ readdirSync,
17
+ readSync,
18
+ realpathSync,
19
+ statSync,
20
+ } from "node:fs";
21
+ import { basename, dirname, join, resolve } from "node:path";
22
+ import { getConfigDir } from "../config";
23
+ import { extractAccountId } from "../oauth/chatgpt";
24
+ import { getCodexHome, readRootTomlString } from "./paths";
25
+ import {
26
+ NativeProfileError,
27
+ NATIVE_PROFILE_JOURNAL_PHASES,
28
+ type EncryptedNativeEnvelopeV1,
29
+ type NativeMainProfileRecordV1,
30
+ type NativeMainProfileVaultV1,
31
+ type NativeProfileKey,
32
+ type NativeProfileKeyProvider,
33
+ type NativeProfilePublic,
34
+ type NativeProfileSwitchJournalV1,
35
+ } from "./native-profile-types";
36
+
37
+ const DOMAIN_HOME = "opencodex-native-profile-home-v1\0";
38
+ const DOMAIN_INSTANCE = "opencodex-native-profile-instance-v1\0";
39
+ const DOMAIN_IDENTITY = "opencodex-native-profile-identity-v1\0";
40
+ const KEYRING_SERVICE = "opencodex.native-main-profile.v1";
41
+ const SHARED_METADATA_DIR = ".opencodex-native-main-profiles";
42
+ const INSTANCE_STAGING_DIR = "native-main-profile-staging";
43
+ const LEGACY_METADATA_DIR = "native-main-profiles";
44
+ const MAX_AUTH_BYTES = 4 * 1024 * 1024;
45
+ export const MAX_NATIVE_PROFILE_METADATA_BYTES = 4 * 1024 * 1024;
46
+ export const MAX_NATIVE_PROFILE_JOURNAL_BYTES = 17 * 1024 * 1024;
47
+ export const MAX_NATIVE_PROFILES = 32;
48
+ 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;
49
+ const HASH_RE = /^[0-9a-f]{64}$/;
50
+ const BOUNDED_READ_TEST_SEAM = Symbol.for("opencodex.native-profile-store.bounded-read-test-seam");
51
+
52
+ export interface NativeProfileContext {
53
+ codexHome: string;
54
+ configDir: string;
55
+ instanceId: string;
56
+ legacyHomeId: string | null;
57
+ rootDir: string;
58
+ stagingRoot: string;
59
+ legacyRootDir: string;
60
+ homeId: string;
61
+ authPath: string;
62
+ vaultPath: string;
63
+ journalPath: string;
64
+ recoveryBlockPath: string;
65
+ stageRegistryPath: string;
66
+ lockPath: string;
67
+ }
68
+
69
+ interface BoundedReadTestSeam {
70
+ beforeOpen?: (path: string) => void;
71
+ afterValidatedOpen?: (path: string, fd: number) => void;
72
+ onBufferAllocated?: (byteLength: number) => void;
73
+ onRead?: (requestedBytes: number, bytesRead: number, totalBytesRead: number) => void;
74
+ }
75
+
76
+ function boundedReadTestSeam(context: NativeProfileContext): BoundedReadTestSeam | undefined {
77
+ return (context as NativeProfileContext & { [BOUNDED_READ_TEST_SEAM]?: BoundedReadTestSeam })[BOUNDED_READ_TEST_SEAM];
78
+ }
79
+
80
+ export interface NativeEnvelopeSnapshot {
81
+ raw: Buffer;
82
+ text: string;
83
+ digest: string;
84
+ accountId: string;
85
+ }
86
+
87
+ export type NativeEnvelopeReadResult =
88
+ | { status: "ok"; envelope: NativeEnvelopeSnapshot }
89
+ | { status: "missing" | "invalid" | "unreadable" };
90
+
91
+ type NativeKeyringSecret = string | Uint8Array | null;
92
+ type NativeKeyringEntry = {
93
+ getSecret(signal: AbortSignal): Promise<NativeKeyringSecret>;
94
+ setSecret(secret: Uint8Array, signal: AbortSignal): Promise<void>;
95
+ };
96
+
97
+ function wipeKeyringSecret(value: NativeKeyringSecret | undefined): void {
98
+ if (!value || typeof value === "string") return;
99
+ try { value.fill(0); } catch { /* cleanup must not replace the primary keyring error */ }
100
+ }
101
+
102
+ export class OsNativeProfileKeyProvider implements NativeProfileKeyProvider {
103
+ private readonly entryFactory: (homeId: string) => Promise<NativeKeyringEntry>;
104
+ private readonly onSecretBufferCreated?: (buffer: Buffer) => void;
105
+
106
+ constructor(options: {
107
+ /** Test-only seam for deterministic keyring cleanup coverage. */
108
+ entryFactory?: (homeId: string) => Promise<NativeKeyringEntry>;
109
+ /** Test-only seam for observing internal secret-buffer ownership. */
110
+ onSecretBufferCreated?: (buffer: Buffer) => void;
111
+ } = {}) {
112
+ this.entryFactory = options.entryFactory ?? (homeId => this.entry(homeId));
113
+ this.onSecretBufferCreated = options.onSecretBufferCreated;
114
+ }
115
+
116
+ private trackSecretBuffer(buffer: Buffer): Buffer {
117
+ this.onSecretBufferCreated?.(buffer);
118
+ return buffer;
119
+ }
120
+
121
+ private async entry(homeId: string): Promise<NativeKeyringEntry> {
122
+ try {
123
+ const { AsyncEntry } = await import("@napi-rs/keyring");
124
+ return new AsyncEntry(KEYRING_SERVICE, homeId) as unknown as NativeKeyringEntry;
125
+ } catch {
126
+ throw new NativeProfileError(
127
+ "KEYRING_UNAVAILABLE",
128
+ "The native OS credential store is unavailable; no plaintext fallback is permitted.",
129
+ 503,
130
+ true,
131
+ );
132
+ }
133
+ }
134
+
135
+ async get(homeId: string): Promise<NativeProfileKey | null> {
136
+ let secret: NativeKeyringSecret | undefined;
137
+ let key: Buffer | null = null;
138
+ try {
139
+ secret = await (await this.entryFactory(homeId)).getSecret(AbortSignal.timeout(8_000));
140
+ if (!secret) return null;
141
+ key = this.trackSecretBuffer(Buffer.from(secret));
142
+ if (key.byteLength !== 32) {
143
+ throw new NativeProfileError("KEYRING_UNAVAILABLE", "The OS credential-store key is invalid.", 503);
144
+ }
145
+ return { keyRef: `${KEYRING_SERVICE}:${homeId}`, key };
146
+ } catch (error) {
147
+ if (key) wipeKeyringSecret(key);
148
+ if (error instanceof NativeProfileError) throw error;
149
+ throw new NativeProfileError(
150
+ "KEYRING_UNAVAILABLE",
151
+ "The native OS credential store could not be read.",
152
+ 503,
153
+ true,
154
+ );
155
+ } finally {
156
+ wipeKeyringSecret(secret);
157
+ }
158
+ }
159
+
160
+ async create(homeId: string): Promise<NativeProfileKey> {
161
+ const existing = await this.get(homeId);
162
+ if (existing) return existing;
163
+ const key = this.trackSecretBuffer(randomBytes(32));
164
+ let stored: NativeKeyringSecret | undefined;
165
+ let verified: Buffer | null = null;
166
+ try {
167
+ const entry = await this.entryFactory(homeId);
168
+ await entry.setSecret(key, AbortSignal.timeout(8_000));
169
+ stored = await entry.getSecret(AbortSignal.timeout(8_000));
170
+ verified = stored ? this.trackSecretBuffer(Buffer.from(stored)) : null;
171
+ if (!verified || verified.byteLength !== key.byteLength || !timingSafeEqual(verified, key)) {
172
+ throw new NativeProfileError("KEYRING_UNAVAILABLE", "The OS credential-store key could not be verified.", 503);
173
+ }
174
+ return { keyRef: `${KEYRING_SERVICE}:${homeId}`, key: this.trackSecretBuffer(Buffer.from(key)) };
175
+ } catch (error) {
176
+ wipeKeyringSecret(verified);
177
+ if (error instanceof NativeProfileError) throw error;
178
+ throw new NativeProfileError(
179
+ "KEYRING_UNAVAILABLE",
180
+ "The native OS credential store could not save the profile key.",
181
+ 503,
182
+ true,
183
+ );
184
+ } finally {
185
+ wipeKeyringSecret(stored);
186
+ wipeKeyringSecret(verified ?? undefined);
187
+ wipeKeyringSecret(key);
188
+ }
189
+ }
190
+ }
191
+
192
+ function canonicalExistingDirectory(path: string, label: string): string {
193
+ const absolute = resolve(path);
194
+ try {
195
+ if (!statSync(absolute).isDirectory()) throw new Error("not a directory");
196
+ return realpathSync.native(absolute);
197
+ } catch {
198
+ throw new NativeProfileError("CODEX_HOME_UNAVAILABLE", `${label} is not an accessible directory.`, 409);
199
+ }
200
+ }
201
+
202
+ function normalizedPath(path: string): string {
203
+ return resolve(path);
204
+ }
205
+
206
+ function canonicalizeDirectoryPath(path: string): string {
207
+ const unresolved: string[] = [];
208
+ let cursor = resolve(path);
209
+ for (;;) {
210
+ try {
211
+ if (!statSync(cursor).isDirectory()) throw new Error("not a directory");
212
+ return join(realpathSync.native(cursor), ...unresolved.reverse());
213
+ } catch (error) {
214
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
215
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The OpenCodex configuration root is not safely accessible.", 409);
216
+ }
217
+ const parent = dirname(cursor);
218
+ if (parent === cursor) {
219
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", "The OpenCodex configuration root is not safely accessible.", 409);
220
+ }
221
+ unresolved.push(basename(cursor));
222
+ cursor = parent;
223
+ }
224
+ }
225
+ }
226
+
227
+ /** Bind keyring and encrypted AAD identity to the case-preserving canonical path. */
228
+ export function nativeProfileHomeId(canonicalCodexHome: string): string {
229
+ return createHash("sha256").update(DOMAIN_HOME).update(canonicalCodexHome).digest("hex");
230
+ }
231
+
232
+ export function legacyWindowsNativeProfileHomeId(canonicalCodexHome: string): string {
233
+ return createHash("sha256").update(DOMAIN_HOME).update(canonicalCodexHome.toLowerCase()).digest("hex");
234
+ }
235
+
236
+ function samePath(left: string, right: string): boolean {
237
+ return normalizedPath(left) === normalizedPath(right);
238
+ }
239
+
240
+ function storageUnsafe(message: string): never {
241
+ throw new NativeProfileError("PROFILE_STORAGE_UNSAFE", message, 409);
242
+ }
243
+
244
+ function assertCanonicalDirectory(path: string, label: string): string {
245
+ try {
246
+ const entry = lstatSync(path);
247
+ if (!entry.isDirectory() || entry.isSymbolicLink()) storageUnsafe(`${label} is not a private directory.`);
248
+ const canonical = realpathSync.native(path);
249
+ if (!samePath(canonical, path)) storageUnsafe(`${label} contains a link or reparse-point substitution.`);
250
+ return canonical;
251
+ } catch (error) {
252
+ if (error instanceof NativeProfileError) throw error;
253
+ storageUnsafe(`${label} is not safely accessible.`);
254
+ }
255
+ }
256
+
257
+ function assertCanonicalFile(path: string, parent: string, label: string): void {
258
+ try {
259
+ const entry = lstatSync(path);
260
+ if (!entry.isFile() || entry.isSymbolicLink()) storageUnsafe(`${label} is not a private regular file.`);
261
+ const canonical = realpathSync.native(path);
262
+ if (!samePath(canonical, path) || !samePath(dirname(canonical), parent)) {
263
+ storageUnsafe(`${label} escaped its private metadata root.`);
264
+ }
265
+ } catch (error) {
266
+ if (error instanceof NativeProfileError) throw error;
267
+ storageUnsafe(`${label} is not safely accessible.`);
268
+ }
269
+ }
270
+
271
+ function pathExists(path: string): boolean {
272
+ try {
273
+ lstatSync(path);
274
+ return true;
275
+ } catch (error) {
276
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
277
+ storageUnsafe("Native-profile storage could not be inspected safely.");
278
+ }
279
+ }
280
+
281
+ function rootHasNativeProfileState(root: string, homeIds: readonly string[], includeStaging: boolean): boolean {
282
+ if (!pathExists(root)) return false;
283
+ let names: string[];
284
+ try {
285
+ const entry = lstatSync(root);
286
+ if (!entry.isDirectory() || entry.isSymbolicLink()) return true;
287
+ const canonical = realpathSync.native(root);
288
+ if (!samePath(canonical, root)) return true;
289
+ names = readdirSync(root);
290
+ } catch {
291
+ return true;
292
+ }
293
+ for (const homeId of homeIds) {
294
+ const exact = new Set([
295
+ `${homeId}.vault.json`,
296
+ `${homeId}.journal.json`,
297
+ `${homeId}.recovery-block.json`,
298
+ ]);
299
+ if (names.some(name => exact.has(name) || name.startsWith(`${homeId}.journal.quarantine-`))) return true;
300
+ if (includeStaging && pathExists(join(root, "staging", homeId))) return true;
301
+ }
302
+ return false;
303
+ }
304
+
305
+ function hasLegacyNativeProfileState(context: NativeProfileContext): boolean {
306
+ const oldIds = context.legacyHomeId && context.legacyHomeId !== context.homeId
307
+ ? [context.homeId, context.legacyHomeId]
308
+ : [context.homeId];
309
+ if (rootHasNativeProfileState(context.legacyRootDir, oldIds, true)) return true;
310
+ return context.legacyHomeId !== null
311
+ && context.legacyHomeId !== context.homeId
312
+ && rootHasNativeProfileState(context.rootDir, [context.legacyHomeId], false);
313
+ }
314
+
315
+ export function assertNoLegacyNativeProfileState(context: NativeProfileContext): void {
316
+ if (!hasLegacyNativeProfileState(context)) return;
317
+ throw new NativeProfileError(
318
+ "LEGACY_PROFILE_STATE",
319
+ "Legacy native-profile state exists under OPENCODEX_HOME. Stop every OpenCodex proxy sharing this CODEX_HOME, then migrate or reset the preview state as documented.",
320
+ 409,
321
+ );
322
+ }
323
+
324
+ /**
325
+ * Validate the one authoritative metadata root without following links. On Windows,
326
+ * Node reports symlinks and junctions through lstat; the realpath equality check also
327
+ * rejects other path-resolving reparse-point substitutions.
328
+ */
329
+ export function assertNativeProfileMetadataLayout(context: NativeProfileContext): void {
330
+ assertNoLegacyNativeProfileState(context);
331
+ const canonicalHome = assertCanonicalDirectory(context.codexHome, "The effective CODEX_HOME");
332
+ const expectedRoot = join(canonicalHome, SHARED_METADATA_DIR);
333
+ if (!samePath(context.rootDir, expectedRoot)) storageUnsafe("The native-profile metadata root is not owned by CODEX_HOME.");
334
+ if (!pathExists(context.rootDir)) return;
335
+ const canonicalRoot = assertCanonicalDirectory(context.rootDir, "The native-profile metadata root");
336
+ let names: string[];
337
+ try {
338
+ names = readdirSync(canonicalRoot);
339
+ } catch {
340
+ storageUnsafe("The native-profile metadata root is not safely readable.");
341
+ }
342
+ for (const name of names) {
343
+ assertCanonicalFile(join(canonicalRoot, name), canonicalRoot, "A native-profile metadata entry");
344
+ }
345
+ }
346
+
347
+ export function assertNativeProfileLockPath(context: NativeProfileContext): void {
348
+ const canonicalHome = assertCanonicalDirectory(context.codexHome, "The effective CODEX_HOME");
349
+ const expected = join(canonicalHome, ".opencodex-native-profile.lock.sqlite");
350
+ if (!samePath(context.lockPath, expected)) storageUnsafe("The native-profile transaction lock is outside CODEX_HOME.");
351
+ if (pathExists(context.lockPath)) assertCanonicalFile(context.lockPath, canonicalHome, "The native-profile transaction lock");
352
+ }
353
+
354
+ export function resolveNativeProfileContext(options: { codexHome?: string; configDir?: string } = {}): NativeProfileContext {
355
+ const codexHome = canonicalExistingDirectory(options.codexHome ?? getCodexHome(), "The effective CODEX_HOME");
356
+ const configDir = canonicalizeDirectoryPath(options.configDir ?? getConfigDir());
357
+ const homeId = nativeProfileHomeId(codexHome);
358
+ const foldedHomeId = process.platform === "win32" ? legacyWindowsNativeProfileHomeId(codexHome) : homeId;
359
+ const instanceId = createHash("sha256").update(DOMAIN_INSTANCE).update(configDir).digest("hex");
360
+ const rootDir = join(codexHome, SHARED_METADATA_DIR);
361
+ return {
362
+ codexHome,
363
+ configDir,
364
+ instanceId,
365
+ legacyHomeId: foldedHomeId === homeId ? null : foldedHomeId,
366
+ rootDir,
367
+ stagingRoot: join(configDir, INSTANCE_STAGING_DIR, homeId),
368
+ legacyRootDir: join(configDir, LEGACY_METADATA_DIR),
369
+ homeId,
370
+ authPath: join(codexHome, "auth.json"),
371
+ vaultPath: join(rootDir, `${homeId}.vault.json`),
372
+ journalPath: join(rootDir, `${homeId}.journal.json`),
373
+ recoveryBlockPath: join(rootDir, `${homeId}.recovery-block.json`),
374
+ stageRegistryPath: join(rootDir, `${homeId}.stages.json`),
375
+ lockPath: join(codexHome, ".opencodex-native-profile.lock.sqlite"),
376
+ };
377
+ }
378
+
379
+ function readBounded(path: string, limit: number, testSeam?: BoundedReadTestSeam): Buffer {
380
+ let fd: number | undefined;
381
+ let failed = false;
382
+ try {
383
+ testSeam?.beforeOpen?.(path);
384
+ const flags = process.platform === "win32"
385
+ ? fsConstants.O_RDONLY
386
+ : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
387
+ fd = openSync(path, flags);
388
+ const opened = fstatSync(fd, { bigint: true });
389
+ if (!opened.isFile() || opened.size > BigInt(limit)) throw new Error("invalid bounded file");
390
+
391
+ if (process.platform === "win32") {
392
+ const pathEntry = lstatSync(path, { bigint: true });
393
+ if (
394
+ !pathEntry.isFile()
395
+ || pathEntry.isSymbolicLink()
396
+ || pathEntry.dev !== opened.dev
397
+ || pathEntry.ino !== opened.ino
398
+ ) throw new Error("invalid bounded file");
399
+ }
400
+
401
+ testSeam?.afterValidatedOpen?.(path, fd);
402
+ const buffer = Buffer.allocUnsafe(limit + 1);
403
+ testSeam?.onBufferAllocated?.(buffer.byteLength);
404
+ let totalBytesRead = 0;
405
+ while (totalBytesRead < buffer.byteLength) {
406
+ const requestedBytes = buffer.byteLength - totalBytesRead;
407
+ const bytesRead = readSync(fd, buffer, totalBytesRead, requestedBytes, totalBytesRead);
408
+ totalBytesRead += bytesRead;
409
+ testSeam?.onRead?.(requestedBytes, bytesRead, totalBytesRead);
410
+ if (bytesRead === 0) break;
411
+ }
412
+ if (totalBytesRead > limit) throw new Error("invalid bounded file");
413
+ return buffer.subarray(0, totalBytesRead);
414
+ } catch (error) {
415
+ failed = true;
416
+ throw error;
417
+ } finally {
418
+ if (fd !== undefined) {
419
+ try {
420
+ closeSync(fd);
421
+ } catch (error) {
422
+ if (!failed) throw error;
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ export function resolveNativeCredentialStoreMode(context: NativeProfileContext): string {
429
+ const path = join(context.codexHome, "config.toml");
430
+ let content: string;
431
+ try {
432
+ content = readBounded(path, 1024 * 1024).toString("utf8");
433
+ } catch (error) {
434
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return "file";
435
+ throw new NativeProfileError("UNSUPPORTED_AUTH_STORE", "The Codex credential-store configuration is unreadable.", 409);
436
+ }
437
+ const mode = readRootTomlString(content, "cli_auth_credentials_store");
438
+ if (mode === null && /^\s*cli_auth_credentials_store\s*=/m.test(content)) {
439
+ throw new NativeProfileError("UNSUPPORTED_AUTH_STORE", "The Codex credential-store mode is invalid.", 409);
440
+ }
441
+ return mode ?? "file";
442
+ }
443
+
444
+ export function requireFileCredentialStore(context: NativeProfileContext): void {
445
+ const mode = resolveNativeCredentialStoreMode(context);
446
+ if (mode !== "file") {
447
+ throw new NativeProfileError(
448
+ "UNSUPPORTED_AUTH_STORE",
449
+ `Native profile switching supports Codex credential-store mode "file" only; current mode is "${mode}".`,
450
+ 409,
451
+ );
452
+ }
453
+ }
454
+
455
+ export function parseNativeEnvelopeBytes(raw: Buffer): NativeEnvelopeSnapshot {
456
+ if (raw.byteLength === 0 || raw.byteLength > MAX_AUTH_BYTES) {
457
+ throw new NativeProfileError("AUTH_INVALID", "The native Codex credential envelope is invalid.", 409);
458
+ }
459
+ const text = raw.toString("utf8");
460
+ if (!Buffer.from(text, "utf8").equals(raw)) {
461
+ throw new NativeProfileError("AUTH_INVALID", "The native Codex credential envelope is not valid UTF-8.", 409);
462
+ }
463
+ try {
464
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, "")) as {
465
+ auth_mode?: unknown;
466
+ tokens?: {
467
+ id_token?: unknown;
468
+ access_token?: unknown;
469
+ refresh_token?: unknown;
470
+ account_id?: unknown;
471
+ };
472
+ };
473
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("root");
474
+ if (parsed.auth_mode !== undefined && parsed.auth_mode !== "chatgpt") throw new Error("mode");
475
+ const tokens = parsed.tokens;
476
+ if (
477
+ !tokens
478
+ || typeof tokens.id_token !== "string" || tokens.id_token.length === 0
479
+ || typeof tokens.access_token !== "string" || tokens.access_token.length === 0
480
+ || typeof tokens.refresh_token !== "string" || tokens.refresh_token.length === 0
481
+ ) throw new Error("tokens");
482
+ const derived = extractAccountId(tokens.id_token, tokens.access_token);
483
+ const explicit = typeof tokens.account_id === "string" && tokens.account_id.trim() ? tokens.account_id.trim() : null;
484
+ if (derived && explicit && derived !== explicit) throw new Error("identity mismatch");
485
+ const accountId = derived ?? explicit;
486
+ if (!accountId) throw new Error("identity");
487
+ return {
488
+ raw,
489
+ text,
490
+ accountId,
491
+ digest: createHash("sha256").update(raw).digest("hex"),
492
+ };
493
+ } catch (error) {
494
+ if (error instanceof NativeProfileError) throw error;
495
+ throw new NativeProfileError("AUTH_INVALID", "The native Codex credential envelope is invalid.", 409);
496
+ }
497
+ }
498
+
499
+ export function readNativeEnvelopeResult(path: string): NativeEnvelopeReadResult {
500
+ let raw: Buffer;
501
+ try {
502
+ raw = readBounded(path, MAX_AUTH_BYTES);
503
+ } catch (error) {
504
+ return { status: (error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "unreadable" };
505
+ }
506
+ try {
507
+ return { status: "ok", envelope: parseNativeEnvelopeBytes(raw) };
508
+ } catch {
509
+ raw.fill(0);
510
+ return { status: "invalid" };
511
+ }
512
+ }
513
+
514
+ export function readNativeEnvelope(path: string): NativeEnvelopeSnapshot {
515
+ const result = readNativeEnvelopeResult(path);
516
+ if (result.status === "ok") return result.envelope;
517
+ const code = result.status === "missing" ? "AUTH_MISSING" : result.status === "invalid" ? "AUTH_INVALID" : "AUTH_UNREADABLE";
518
+ throw new NativeProfileError(code, `The native Codex credential envelope is ${result.status}.`, 409);
519
+ }
520
+
521
+ export function nativeIdentityHash(key: Uint8Array, accountId: string): string {
522
+ return createHmac("sha256", key).update(DOMAIN_IDENTITY).update(accountId).digest("hex");
523
+ }
524
+
525
+ export function nativeIdentityHint(identityHash: string): string {
526
+ return `account-${identityHash.slice(0, 8)}`;
527
+ }
528
+
529
+ function aad(context: NativeProfileContext, profileId: string, identityHash: string, digest: string): Buffer {
530
+ return Buffer.from(JSON.stringify([1, context.homeId, profileId, identityHash, digest]), "utf8");
531
+ }
532
+
533
+ export function encryptNativeEnvelope(
534
+ context: NativeProfileContext,
535
+ profileId: string,
536
+ identityHash: string,
537
+ envelope: NativeEnvelopeSnapshot,
538
+ key: NativeProfileKey,
539
+ ): EncryptedNativeEnvelopeV1 {
540
+ const nonce = randomBytes(12);
541
+ const cipher = createCipheriv("aes-256-gcm", key.key, nonce);
542
+ cipher.setAAD(aad(context, profileId, identityHash, envelope.digest));
543
+ const ciphertext = Buffer.concat([cipher.update(envelope.raw), cipher.final()]);
544
+ const tag = cipher.getAuthTag();
545
+ return {
546
+ cipher: "aes-256-gcm",
547
+ keyRef: key.keyRef,
548
+ nonce: nonce.toString("base64"),
549
+ ciphertext: ciphertext.toString("base64"),
550
+ tag: tag.toString("base64"),
551
+ envelopeSha256: envelope.digest,
552
+ };
553
+ }
554
+
555
+ export function decryptNativeEnvelope(
556
+ context: NativeProfileContext,
557
+ profileId: string,
558
+ identityHash: string,
559
+ payload: EncryptedNativeEnvelopeV1,
560
+ key: NativeProfileKey,
561
+ ): NativeEnvelopeSnapshot {
562
+ let raw: Buffer | null = null;
563
+ try {
564
+ if (payload.keyRef !== key.keyRef || payload.cipher !== "aes-256-gcm") throw new Error("key");
565
+ const decipher = createDecipheriv("aes-256-gcm", key.key, Buffer.from(payload.nonce, "base64"));
566
+ decipher.setAAD(aad(context, profileId, identityHash, payload.envelopeSha256));
567
+ decipher.setAuthTag(Buffer.from(payload.tag, "base64"));
568
+ raw = Buffer.concat([decipher.update(Buffer.from(payload.ciphertext, "base64")), decipher.final()]);
569
+ const parsed = parseNativeEnvelopeBytes(raw);
570
+ if (parsed.digest !== payload.envelopeSha256) throw new Error("digest");
571
+ return parsed;
572
+ } catch {
573
+ raw?.fill(0);
574
+ throw new NativeProfileError("PROFILE_DECRYPT_FAILED", "The selected native profile could not be decrypted or verified.", 409);
575
+ }
576
+ }
577
+
578
+ function isEncryptedPayload(value: unknown): value is EncryptedNativeEnvelopeV1 {
579
+ if (!value || typeof value !== "object") return false;
580
+ const v = value as Record<string, unknown>;
581
+ return v.cipher === "aes-256-gcm"
582
+ && typeof v.keyRef === "string" && v.keyRef.length > 0
583
+ && typeof v.nonce === "string" && v.nonce.length > 0
584
+ && typeof v.ciphertext === "string" && v.ciphertext.length > 0
585
+ && typeof v.tag === "string" && v.tag.length > 0
586
+ && typeof v.envelopeSha256 === "string" && HASH_RE.test(v.envelopeSha256);
587
+ }
588
+
589
+ function parseVaultObject(value: unknown, homeId: string): NativeMainProfileVaultV1 {
590
+ if (!value || typeof value !== "object") throw new Error("root");
591
+ const vault = value as NativeMainProfileVaultV1;
592
+ if (vault.version !== 1 || vault.homeId !== homeId || !Number.isSafeInteger(vault.revision) || vault.revision < 0) throw new Error("header");
593
+ if (!Array.isArray(vault.profiles) || vault.profiles.length > MAX_NATIVE_PROFILES) throw new Error("profiles");
594
+ if (vault.activeProfileId !== null && (typeof vault.activeProfileId !== "string" || !UUID_RE.test(vault.activeProfileId))) throw new Error("active");
595
+ const ids = new Set<string>();
596
+ const labels = new Set<string>();
597
+ const identities = new Set<string>();
598
+ let activeCount = 0;
599
+ for (const profile of vault.profiles) {
600
+ if (!profile || typeof profile !== "object" || !UUID_RE.test(profile.id)) throw new Error("id");
601
+ const normalizedId = nativeProfileSelectorKey(profile.id);
602
+ if (ids.has(normalizedId) || labels.has(normalizedId)) throw new Error("id selector");
603
+ const label = normalizedNativeProfileLabel(profile.label);
604
+ if (!label) throw new Error("label");
605
+ profile.label = label;
606
+ const normalizedLabel = nativeProfileSelectorKey(label);
607
+ if (labels.has(normalizedLabel) || ids.has(normalizedLabel)) throw new Error("label selector");
608
+ if (!HASH_RE.test(profile.identityHash) || identities.has(profile.identityHash)) throw new Error("identity");
609
+ if (profile.identityHint !== nativeIdentityHint(profile.identityHash)) throw new Error("hint");
610
+ if (profile.state === "active") {
611
+ activeCount += 1;
612
+ if (profile.payload !== null || profile.id !== vault.activeProfileId) throw new Error("active payload");
613
+ } else if (profile.state === "inactive") {
614
+ if (!isEncryptedPayload(profile.payload)) throw new Error("inactive payload");
615
+ } else throw new Error("state");
616
+ ids.add(normalizedId);
617
+ labels.add(normalizedLabel);
618
+ identities.add(profile.identityHash);
619
+ }
620
+ if ((vault.profiles.length === 0 && vault.activeProfileId !== null) || (vault.profiles.length > 0 && activeCount !== 1)) throw new Error("active count");
621
+ return vault;
622
+ }
623
+
624
+ function encryptedPayloadsMatch(
625
+ left: EncryptedNativeEnvelopeV1 | null,
626
+ right: EncryptedNativeEnvelopeV1 | null,
627
+ ): boolean {
628
+ if (left === null || right === null) return left === right;
629
+ return left.cipher === right.cipher
630
+ && left.keyRef === right.keyRef
631
+ && left.nonce === right.nonce
632
+ && left.ciphertext === right.ciphertext
633
+ && left.tag === right.tag
634
+ && left.envelopeSha256 === right.envelopeSha256;
635
+ }
636
+
637
+ function profileMetadataMatches(
638
+ before: NativeMainProfileRecordV1,
639
+ after: NativeMainProfileRecordV1,
640
+ ): boolean {
641
+ return before.id === after.id
642
+ && before.label === after.label
643
+ && before.identityHash === after.identityHash
644
+ && before.identityHint === after.identityHint
645
+ && before.createdAt === after.createdAt;
646
+ }
647
+
648
+ function profileRecordsMatch(
649
+ before: NativeMainProfileRecordV1,
650
+ after: NativeMainProfileRecordV1,
651
+ ): boolean {
652
+ return profileMetadataMatches(before, after)
653
+ && before.state === after.state
654
+ && before.updatedAt === after.updatedAt
655
+ && encryptedPayloadsMatch(before.payload, after.payload);
656
+ }
657
+
658
+ function hasValidJournalSwitchInvariant(journal: NativeProfileSwitchJournalV1): boolean {
659
+ if (
660
+ journal.sourceProfileId === journal.targetProfileId
661
+ || journal.sourceIdentityHash === journal.targetIdentityHash
662
+ || journal.beforeVault.activeProfileId !== journal.sourceProfileId
663
+ || journal.afterVault.activeProfileId !== journal.targetProfileId
664
+ || journal.beforeVault.profiles.length !== journal.afterVault.profiles.length
665
+ || (
666
+ journal.afterVault.revision !== journal.beforeVault.revision + 1
667
+ && journal.afterVault.revision !== journal.beforeVault.revision
668
+ )
669
+ ) return false;
670
+
671
+ const beforeProfiles = new Map(journal.beforeVault.profiles.map(profile => [profile.id, profile]));
672
+ const afterProfiles = new Map(journal.afterVault.profiles.map(profile => [profile.id, profile]));
673
+ const beforeSource = beforeProfiles.get(journal.sourceProfileId);
674
+ const beforeTarget = beforeProfiles.get(journal.targetProfileId);
675
+ const afterSource = afterProfiles.get(journal.sourceProfileId);
676
+ const afterTarget = afterProfiles.get(journal.targetProfileId);
677
+
678
+ if (
679
+ !beforeSource || !beforeTarget || !afterSource || !afterTarget
680
+ || beforeSource.identityHash !== journal.sourceIdentityHash
681
+ || afterSource.identityHash !== journal.sourceIdentityHash
682
+ || beforeTarget.identityHash !== journal.targetIdentityHash
683
+ || afterTarget.identityHash !== journal.targetIdentityHash
684
+ || beforeSource.state !== "active" || beforeSource.payload !== null
685
+ || beforeTarget.state !== "inactive" || !encryptedPayloadsMatch(beforeTarget.payload, journal.targetPayload)
686
+ || afterSource.state !== "inactive" || !encryptedPayloadsMatch(afterSource.payload, journal.sourcePayload)
687
+ || afterTarget.state !== "active" || afterTarget.payload !== null
688
+ || !profileMetadataMatches(beforeSource, afterSource)
689
+ || !profileMetadataMatches(beforeTarget, afterTarget)
690
+ || afterSource.updatedAt !== afterTarget.updatedAt
691
+ ) return false;
692
+
693
+ for (const beforeProfile of journal.beforeVault.profiles) {
694
+ const afterProfile = afterProfiles.get(beforeProfile.id);
695
+ if (!afterProfile) return false;
696
+ if (beforeProfile.id !== journal.sourceProfileId
697
+ && beforeProfile.id !== journal.targetProfileId
698
+ && !profileRecordsMatch(beforeProfile, afterProfile)) return false;
699
+ }
700
+ return true;
701
+ }
702
+
703
+ export function readNativeProfileVault(context: NativeProfileContext): NativeMainProfileVaultV1 | null {
704
+ assertNativeProfileMetadataLayout(context);
705
+ let raw: Buffer;
706
+ try {
707
+ raw = readBounded(context.vaultPath, MAX_NATIVE_PROFILE_METADATA_BYTES, boundedReadTestSeam(context));
708
+ } catch (error) {
709
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
710
+ throw new NativeProfileError("VAULT_INVALID", "The encrypted native-profile vault is unreadable.", 409);
711
+ }
712
+ try {
713
+ return parseVaultObject(JSON.parse(raw.toString("utf8")), context.homeId);
714
+ } catch {
715
+ throw new NativeProfileError("VAULT_INVALID", "The encrypted native-profile vault is invalid.", 409);
716
+ }
717
+ }
718
+
719
+ type PrivateFileState = "missing" | "present" | "unreadable";
720
+
721
+ function privateFileState(path: string): PrivateFileState {
722
+ try {
723
+ const stat = lstatSync(path);
724
+ return stat.isFile() && !stat.isSymbolicLink() ? "present" : "unreadable";
725
+ } catch (error) {
726
+ return (error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "unreadable";
727
+ }
728
+ }
729
+
730
+ export type NativeProfileRecoveryState = "none" | "journal" | "manual" | "unreadable";
731
+
732
+ export function probeNativeProfileRecoveryState(context: NativeProfileContext): NativeProfileRecoveryState {
733
+ try { assertNativeProfileMetadataLayout(context); } catch { return "unreadable"; }
734
+ const block = privateFileState(context.recoveryBlockPath);
735
+ if (block === "unreadable") return "unreadable";
736
+ if (block === "present") return "manual";
737
+ const journal = privateFileState(context.journalPath);
738
+ if (journal === "unreadable") return "unreadable";
739
+ return journal === "present" ? "journal" : "none";
740
+ }
741
+
742
+ export type NativeProfileJournalInspection =
743
+ | { status: "missing" }
744
+ | { status: "valid"; journal: NativeProfileSwitchJournalV1 }
745
+ | { status: "invalid" };
746
+
747
+ export function inspectNativeProfileJournal(context: NativeProfileContext): NativeProfileJournalInspection {
748
+ try { assertNativeProfileMetadataLayout(context); } catch { return { status: "invalid" }; }
749
+ const state = privateFileState(context.journalPath);
750
+ if (state === "missing") return { status: "missing" };
751
+ if (state === "unreadable") return { status: "invalid" };
752
+ try {
753
+ const journal = JSON.parse(
754
+ readBounded(
755
+ context.journalPath,
756
+ MAX_NATIVE_PROFILE_JOURNAL_BYTES,
757
+ boundedReadTestSeam(context),
758
+ ).toString("utf8"),
759
+ ) as NativeProfileSwitchJournalV1;
760
+ if (
761
+ journal.version !== 1 || journal.homeId !== context.homeId || !UUID_RE.test(journal.transactionId)
762
+ || !NATIVE_PROFILE_JOURNAL_PHASES.includes(journal.phase)
763
+ || !UUID_RE.test(journal.sourceProfileId) || !UUID_RE.test(journal.targetProfileId)
764
+ || !HASH_RE.test(journal.sourceIdentityHash) || !HASH_RE.test(journal.targetIdentityHash)
765
+ || !isEncryptedPayload(journal.sourcePayload) || !isEncryptedPayload(journal.targetPayload)
766
+ ) throw new Error("journal");
767
+ journal.beforeVault = parseVaultObject(journal.beforeVault, context.homeId);
768
+ journal.afterVault = parseVaultObject(journal.afterVault, context.homeId);
769
+ if (!hasValidJournalSwitchInvariant(journal)) throw new Error("journal invariant");
770
+ return { status: "valid", journal };
771
+ } catch {
772
+ return { status: "invalid" };
773
+ }
774
+ }
775
+
776
+ export function readNativeProfileJournal(context: NativeProfileContext): NativeProfileSwitchJournalV1 | null {
777
+ const inspection = inspectNativeProfileJournal(context);
778
+ if (inspection.status === "missing") return null;
779
+ if (inspection.status === "valid") return inspection.journal;
780
+ throw new NativeProfileError(
781
+ "RECOVERY_REQUIRED",
782
+ "The native-profile recovery journal is invalid. Run account main recover --rollback --yes to quarantine it.",
783
+ 409,
784
+ );
785
+ }
786
+
787
+ export function serializeNativeProfileMetadata(value: unknown): string {
788
+ const serialized = JSON.stringify(value, null, 2) + "\n";
789
+ if (Buffer.byteLength(serialized, "utf8") > MAX_NATIVE_PROFILE_METADATA_BYTES) {
790
+ throw new NativeProfileError(
791
+ "PROFILE_METADATA_TOO_LARGE",
792
+ "Native-profile metadata exceeds the 4 MiB recovery limit; no credential write was made.",
793
+ 409,
794
+ );
795
+ }
796
+ return serialized;
797
+ }
798
+
799
+ export function serializeNativeProfileJournal(journal: NativeProfileSwitchJournalV1): string {
800
+ const serialized = JSON.stringify(journal) + "\n";
801
+ if (Buffer.byteLength(serialized, "utf8") > MAX_NATIVE_PROFILE_JOURNAL_BYTES) {
802
+ throw new NativeProfileError(
803
+ "PROFILE_METADATA_TOO_LARGE",
804
+ "The native-profile recovery journal exceeds the 17 MiB limit; no credential write was made.",
805
+ 409,
806
+ );
807
+ }
808
+ return serialized;
809
+ }
810
+
811
+ const FORBIDDEN_PROFILE_LABEL_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
812
+
813
+ /** Canonical key shared by persisted ID/label collision checks and switch selectors. */
814
+ export function nativeProfileSelectorKey(value: string): string {
815
+ return value.normalize("NFC").trim().toLowerCase();
816
+ }
817
+
818
+ function normalizedNativeProfileLabel(value: unknown): string | null {
819
+ if (typeof value !== "string" || FORBIDDEN_PROFILE_LABEL_RE.test(value)) return null;
820
+ const normalized = value.normalize("NFC").trim();
821
+ if (!normalized || normalized.length > 64 || FORBIDDEN_PROFILE_LABEL_RE.test(normalized)) return null;
822
+ return normalized;
823
+ }
824
+
825
+ export function validateNativeProfileLabel(label: string): string {
826
+ const trimmed = normalizedNativeProfileLabel(label);
827
+ if (!trimmed) {
828
+ throw new NativeProfileError("INVALID_REQUEST", "Profile labels must contain 1-64 printable characters.", 400);
829
+ }
830
+ return trimmed;
831
+ }
832
+
833
+ export function assertUniqueNativeProfileLabel(
834
+ vault: Pick<NativeMainProfileVaultV1, "profiles">,
835
+ label: string,
836
+ excludeId?: string,
837
+ additionalIds: readonly string[] = [],
838
+ ): void {
839
+ const selector = nativeProfileSelectorKey(label);
840
+ if (vault.profiles.some(
841
+ profile => profile.id !== excludeId && nativeProfileSelectorKey(profile.label) === selector,
842
+ )) {
843
+ throw new NativeProfileError("PROFILE_ALREADY_EXISTS", "A native profile already uses that label.", 409);
844
+ }
845
+ if (
846
+ vault.profiles.some(profile => nativeProfileSelectorKey(profile.id) === selector)
847
+ || additionalIds.some(id => nativeProfileSelectorKey(id) === selector)
848
+ ) {
849
+ throw new NativeProfileError("PROFILE_ALREADY_EXISTS", "A native profile ID already uses that selector.", 409);
850
+ }
851
+ }
852
+
853
+ export function publicNativeProfile(profile: NativeMainProfileRecordV1): NativeProfilePublic {
854
+ return { id: profile.id, label: profile.label, identityHint: profile.identityHint, state: profile.state };
855
+ }