@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,269 @@
1
+ /**
2
+ * The filesystem seam shared by the reader and the writer.
3
+ *
4
+ * It lives in WP2 rather than beside the writer because `readIntegrationState`
5
+ * needs it too, and a reader that disagreed with the writer about what counts
6
+ * as an absent file is exactly how an unreadable config gets overwritten.
7
+ *
8
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §5-6.
9
+ */
10
+ import { mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
11
+ import type { ConfigFormat } from "../clients/config-export";
12
+ import { MAX_JSON_NESTING } from "./serialize";
13
+ import { atomicWriteFile } from "../config";
14
+ import type { JournalEntry } from "./journal";
15
+ import type { OwnershipRecord } from "./ownership";
16
+ import type { IntegrationClientId } from "./registry";
17
+
18
+ /**
19
+ * A parse failure sentinel. This is deliberately ONE symbol exported from ONE
20
+ * module: two `Symbol("parse-failed")` calls produce different values, so a
21
+ * second declaration would not duplicate code — it would make every comparison
22
+ * against it fail, and an unparseable config would be classified `absent` and
23
+ * then overwritten.
24
+ */
25
+ export const PARSE_FAILED = Symbol("parse-failed");
26
+
27
+ /**
28
+ * What `JSON.parse` has already discarded by the time we hold the parsed value,
29
+ * and a rewrite would therefore silently change. Both classes are invisible in
30
+ * the parsed object, which is why this scans the RAW text — same reasoning as
31
+ * the TOML inf/nan guard below.
32
+ *
33
+ * Numbers: `1e999` overflows to Infinity (a rewrite bakes in `null` — the merge
34
+ * layer's JSON clone does it even before the serializer could refuse), `1e-9999`
35
+ * underflows to `+0`, an integer literal may have been rounded (a rewrite then
36
+ * hands consumers that read JSON integers exactly — python, jq, BigInt revivers
37
+ * — a different value), and `-0` re-serializes as `0`. Only literals whose value
38
+ * actually changed are refused: `1e21`, `1e-320` or 2^54 round-trip exactly and
39
+ * stay usable.
40
+ *
41
+ * Duplicate members: `{"a":1,"a":2}` parses to a single `a`, so rewriting the
42
+ * document DELETES the earlier member. That is content loss, not the formatting
43
+ * normalization we promise, and this classifier is what makes the rewrite of a
44
+ * user-edited file reachable at all — so it fails closed here.
45
+ *
46
+ * Depth: the same pass counts container nesting against MAX_JSON_NESTING
47
+ * (shared with the serializer, see serialize.ts). JSON.parse handles hundreds
48
+ * of thousands of levels iteratively, but the downstream merge and
49
+ * JSON.stringify recurse — a 100KB file nested 50k deep sailed through parse
50
+ * and guard, then blew up serialization with a raw RangeError after a
51
+ * multi-GB allocation spike. A hand-written scan rather than a JSON.parse
52
+ * source-access reviver on purpose: the reviver walk recurses internally, so
53
+ * its depth limit would be an unpredictable stack size instead of this
54
+ * deterministic ceiling.
55
+ */
56
+ function jsonTextSafeToRewrite(text: string): boolean {
57
+ /**
58
+ * One frame per open container; a Set of member names for objects, null for
59
+ * arrays. Its length is the current nesting depth.
60
+ */
61
+ const containers: Array<Set<string> | null> = [];
62
+ /** The most recent string literal — the member name if a `:` follows. */
63
+ let lastString: string | null = null;
64
+ let inString = false;
65
+ let escaped = false;
66
+ let stringStart = 0;
67
+ for (let i = 0; i < text.length; i += 1) {
68
+ const ch = text[i]!;
69
+ if (inString) {
70
+ if (escaped) escaped = false;
71
+ else if (ch === "\\") escaped = true;
72
+ else if (ch === "\"") {
73
+ inString = false;
74
+ lastString = text.slice(stringStart, i + 1);
75
+ }
76
+ continue;
77
+ }
78
+ if (ch === "\"") { inString = true; stringStart = i; continue; }
79
+ if (ch === "{" || ch === "[") {
80
+ containers.push(ch === "{" ? new Set<string>() : null);
81
+ if (containers.length > MAX_JSON_NESTING) return false;
82
+ lastString = null;
83
+ continue;
84
+ }
85
+ if (ch === "}" || ch === "]") { containers.pop(); lastString = null; continue; }
86
+ if (ch === ":") {
87
+ const members = containers[containers.length - 1];
88
+ if (members && lastString !== null) {
89
+ /*
90
+ * Decoded, not raw: `"a"` and `"a"` are spellings of ONE member,
91
+ * and JSON.parse keeps only the last of them.
92
+ */
93
+ let name: string;
94
+ try { name = JSON.parse(lastString) as string; } catch { return false; }
95
+ if (members.has(name)) return false;
96
+ members.add(name);
97
+ }
98
+ lastString = null;
99
+ continue;
100
+ }
101
+ if (ch !== "-" && (ch < "0" || ch > "9")) continue;
102
+ let end = i + 1;
103
+ while (end < text.length && /[0-9+\-.eE]/.test(text[end]!)) end += 1;
104
+ const literal = text.slice(i, end);
105
+ i = end - 1;
106
+ const value = Number(literal);
107
+ if (!Number.isFinite(value)) return false;
108
+ if (value === 0) {
109
+ /*
110
+ * `-0` (re-serializes as `0`) and underflow: `1e-9999` is a nonzero
111
+ * value the parse already flattened to `+0`. The significand alone
112
+ * decides, so genuine zero spellings (`0`, `0.0`, `0e10`) stay usable.
113
+ */
114
+ if (literal.startsWith("-") || /[1-9]/.test(literal.split(/[eE]/)[0]!)) return false;
115
+ }
116
+ /*
117
+ * Deliberately plain digit runs only. They are the one spelling real
118
+ * consumers read with exact integer semantics (python's json yields an
119
+ * arbitrary-precision int, jq preserves big integer literals), so baking
120
+ * in the rounded double changes what those consumers extract. Decimal or
121
+ * exponent spellings of the same value (`9007199254740993e0`, `…3.0`) are
122
+ * float semantics for every consumer — they round identically before and
123
+ * after a rewrite, and shortest-round-trip stringify preserves what any
124
+ * reader can observe, so refusing them would only manufacture dead ends
125
+ * (`1e308` is not exactly representable either, yet rewrites losslessly
126
+ * for every possible reader).
127
+ */
128
+ const digits = literal[0] === "-" ? literal.slice(1) : literal;
129
+ if (/^[0-9]{16,}$/.test(digits) && BigInt(literal) !== BigInt(value)) return false;
130
+ }
131
+ return true;
132
+ }
133
+
134
+ /** Parse a client config, tolerating absence. PARSE_FAILED on garbage. */
135
+ export function parseConfig(text: string | null, format: ConfigFormat): unknown | typeof PARSE_FAILED {
136
+ if (text === null || text.trim().length === 0) return {};
137
+ try {
138
+ switch (format) {
139
+ case "json": {
140
+ /*
141
+ * Scanned BEFORE parsing: the guard is text-only, and refusing first
142
+ * means a hostile document is never materialized — the depth ceiling
143
+ * would otherwise cap the rewrite only after JSON.parse had already
144
+ * built the 50k-deep object graph. The outcome is unchanged: invalid
145
+ * JSON still returns PARSE_FAILED, from the catch below.
146
+ */
147
+ if (!jsonTextSafeToRewrite(text)) return PARSE_FAILED;
148
+ return JSON.parse(text);
149
+ }
150
+ case "json5": return Bun.JSON5.parse(text);
151
+ case "yaml": return Bun.YAML.parse(text);
152
+ case "toml": {
153
+ /*
154
+ * Bun's TOML parser mangles the special floats before we ever see the
155
+ * document: `inf` comes back as the STRING "inf", `-inf` as the number
156
+ * 0, and `nan` as "nan". Re-serializing that would write those
157
+ * corruptions back to the user's file while reporting success — a
158
+ * silent value change is worse than a refusal, so a document
159
+ * containing them is treated as one we cannot safely rewrite.
160
+ *
161
+ * Detected on the raw text, because by the time it is parsed the
162
+ * evidence is gone.
163
+ */
164
+ if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED;
165
+ return Bun.TOML.parse(text);
166
+ }
167
+ }
168
+ } catch {
169
+ return PARSE_FAILED;
170
+ }
171
+ }
172
+
173
+ export type ReadResult =
174
+ | { kind: "text"; text: string }
175
+ | { kind: "missing" }
176
+ | { kind: "failed"; code?: string };
177
+
178
+ export type StatKind = "file" | "dir" | "other" | "missing" | "failed";
179
+
180
+ export interface IntegrationIO {
181
+ /**
182
+ * ONLY a missing file yields `missing`. Every other failure (EACCES, EPERM,
183
+ * EISDIR, …) yields `failed`, which callers must treat as unsafe.
184
+ */
185
+ readText: (path: string) => ReadResult;
186
+ /** `failed` is distinct from `missing` for the same reason. */
187
+ statKind: (path: string) => StatKind;
188
+ writeText: (path: string, text: string) => void;
189
+ removeFile: (path: string) => void;
190
+ mkdirp: (path: string) => void;
191
+ now: () => number;
192
+ /** Bookkeeping seams, bound to one store so a test can redirect them. */
193
+ appendJournal: (entry: JournalEntry) => void;
194
+ putRecord: (record: OwnershipRecord) => void;
195
+ dropRecord: (clientId: IntegrationClientId) => void;
196
+ }
197
+
198
+ export type TargetState =
199
+ | { ok: true; before: string | null }
200
+ | { ok: false; why: "not-regular-file" | "read-failed" };
201
+
202
+ /**
203
+ * Read the target and classify the three failure shapes correctly.
204
+ *
205
+ * The subtle case is a `stat` that succeeds as a file and a `read` that then
206
+ * fails: that is a real file we cannot see, and treating it as absence is how
207
+ * an unreadable config gets clobbered.
208
+ */
209
+ export function loadTarget(io: IntegrationIO, configPath: string): TargetState {
210
+ const kind = io.statKind(configPath);
211
+ if (kind === "missing") return { ok: true, before: null };
212
+ if (kind === "failed") return { ok: false, why: "read-failed" };
213
+ if (kind !== "file") return { ok: false, why: "not-regular-file" };
214
+ const read = io.readText(configPath);
215
+ if (read.kind === "text") return { ok: true, before: read.text };
216
+ if (read.kind === "failed") return { ok: false, why: "read-failed" };
217
+ // Raced deletion between the stat and the read.
218
+ return { ok: true, before: null };
219
+ }
220
+
221
+ /**
222
+ * Filesystem half of the seam, split from the store-bound half so there is one
223
+ * place that decides what a failed read or stat MEANS, and one place that
224
+ * decides WHERE bookkeeping goes.
225
+ */
226
+ export function fileIO(): Omit<IntegrationIO, "appendJournal" | "putRecord" | "dropRecord"> {
227
+ return {
228
+ readText: path => {
229
+ try {
230
+ return { kind: "text", text: readFileSync(path, "utf8") };
231
+ } catch (error) {
232
+ const code = (error as NodeJS.ErrnoException).code;
233
+ return code === "ENOENT" ? { kind: "missing" } : { kind: "failed", ...(code ? { code } : {}) };
234
+ }
235
+ },
236
+ statKind: path => {
237
+ try {
238
+ const stats = statSync(path);
239
+ return stats.isFile() ? "file" : stats.isDirectory() ? "dir" : "other";
240
+ } catch (error) {
241
+ return (error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "failed";
242
+ }
243
+ },
244
+ writeText: (path, text) => atomicWriteFile(path, text),
245
+ removeFile: path => rmSync(path, { force: true }),
246
+ mkdirp: path => mkdirSync(path, { recursive: true, mode: 0o700 }),
247
+ now: () => Date.now(),
248
+ };
249
+ }
250
+
251
+ /**
252
+ * The full seam: filesystem behavior plus bookkeeping bound to ONE store.
253
+ *
254
+ * Kept together so the IO a writer uses and the store it journals into can
255
+ * never point at different roots — that split is what let an "isolated"
256
+ * operation mutate the real state while a test believed otherwise.
257
+ */
258
+ export function defaultIntegrationIO(store: {
259
+ appendJournal: IntegrationIO["appendJournal"];
260
+ putRecord: IntegrationIO["putRecord"];
261
+ dropRecord: IntegrationIO["dropRecord"];
262
+ }): IntegrationIO {
263
+ return {
264
+ ...fileIO(),
265
+ appendJournal: entry => store.appendJournal(entry),
266
+ putRecord: record => store.putRecord(record),
267
+ dropRecord: clientId => store.dropRecord(clientId),
268
+ };
269
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * The operation log, its snapshots, and the retention bookkeeping around them.
3
+ *
4
+ * Two ordering rules carry the safety here:
5
+ *
6
+ * 1. `appendOperation` commits the row and NOTHING else. Pruning runs after,
7
+ * best-effort, because a GC failure that looked like an append failure would
8
+ * make the writer compensate for an operation that already succeeded.
9
+ * 2. Snapshots can hold the user's own credentials — we copy their file
10
+ * verbatim — so they go through `atomicWriteFile`, which applies 0600 plus
11
+ * Windows ACL hardening.
12
+ *
13
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §4.
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { appendFileSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
17
+ import { join, resolve, sep } from "node:path";
18
+ import { atomicWriteFile } from "../config";
19
+ import { ensureDir, fingerprint, integrationsDir, type OwnershipRecord } from "./ownership";
20
+ import { isIntegrationClientId, type IntegrationClientId } from "./registry";
21
+
22
+ export type OperationKind = "apply" | "disable" | "refresh" | "restore";
23
+
24
+ /**
25
+ * Tagged so "the file did not exist" and "the snapshot was collected" stay
26
+ * distinguishable. `null` conflated them, and only one of the two is
27
+ * recoverable — restoring an operation that created a file means deleting it.
28
+ */
29
+ export type SnapshotRef =
30
+ | { kind: "none" }
31
+ | { kind: "stored"; relPath: string }
32
+ | { kind: "expired" };
33
+
34
+ export interface JournalEntry {
35
+ opId: string;
36
+ clientId: IntegrationClientId;
37
+ kind: OperationKind;
38
+ at: string;
39
+ configPath: string;
40
+ snapshot: SnapshotRef;
41
+ /** Fingerprint of the file AFTER this op; "" when the op left no file. */
42
+ resultFingerprint: string;
43
+ /** True when the op's result was file absence — restore then means "delete". */
44
+ resultAbsent: boolean;
45
+ /**
46
+ * Ownership as it stood BEFORE this operation. Restore puts this back
47
+ * alongside the bytes, so provenance always describes the file it came with
48
+ * and is never re-derived from a provider-id prefix — which would silently
49
+ * adopt a user's own `opencodex/...` entry.
50
+ */
51
+ priorRecord: OwnershipRecord | null;
52
+ }
53
+
54
+ export const SNAPSHOT_RETENTION = 10;
55
+
56
+ /**
57
+ * Does the file on disk still hold what this operation left behind?
58
+ *
59
+ * The one place that answers it, because two places answered differently. An
60
+ * operation whose result was ABSENCE records `resultAbsent: true` and an empty
61
+ * `resultFingerprint`; the route compared a missing file to `""` and called it
62
+ * a match, while restore hashed `""` into a real digest and called the same
63
+ * unchanged absence a drift. So the journal offered Undo and the restore
64
+ * route then demanded a confirmation for edits nobody had made.
65
+ *
66
+ * `currentText` is `null` for a missing file — not `""`, which is a file that
67
+ * exists and is empty. The distinction is the whole point.
68
+ */
69
+ export function matchesOperationResult(entry: JournalEntry, currentText: string | null): boolean {
70
+ if (entry.resultAbsent) return currentText === null;
71
+ if (currentText === null) return false;
72
+ return fingerprint(currentText) === entry.resultFingerprint;
73
+ }
74
+
75
+ export function newOpId(): string {
76
+ return randomUUID();
77
+ }
78
+
79
+ function journalPath(dir: string): string {
80
+ return join(dir, "journal.jsonl");
81
+ }
82
+
83
+ /**
84
+ * A snapshot file name must be ONE path component we produced, never a value
85
+ * that walks anywhere. `opId` comes from `randomUUID()` in normal operation,
86
+ * but it also arrives from a persisted journal row, and `relPath` is read
87
+ * straight off disk — a row carrying `../../escaped` would otherwise let
88
+ * capture write outside the store and read-back read an arbitrary file.
89
+ */
90
+ function assertSafeComponent(value: string, what: string): string {
91
+ if (value.length === 0 || value === "." || value === ".."
92
+ || value.includes("/") || value.includes("\\") || value.includes("\0")) {
93
+ throw new Error(`unsafe ${what}: ${JSON.stringify(value)}`);
94
+ }
95
+ return value;
96
+ }
97
+
98
+ /** Resolve `relative` under `root`, refusing anything that escapes it. */
99
+ function containedPath(root: string, ...segments: string[]): string {
100
+ const target = resolve(root, ...segments);
101
+ const base = resolve(root);
102
+ if (target !== base && !target.startsWith(base + sep)) {
103
+ throw new Error(`path escapes the integration store: ${target}`);
104
+ }
105
+ return target;
106
+ }
107
+
108
+ function snapshotDir(clientId: IntegrationClientId, dir: string): string {
109
+ return join(dir, "snapshots", clientId);
110
+ }
111
+
112
+ export function captureSnapshot(
113
+ clientId: IntegrationClientId,
114
+ opId: string,
115
+ text: string | null,
116
+ dir: string = integrationsDir(),
117
+ ): SnapshotRef {
118
+ if (text === null) return { kind: "none" };
119
+ const target = containedPath(dir, "snapshots", assertSafeComponent(clientId, "clientId"), assertSafeComponent(opId, "opId"));
120
+ ensureDir(target);
121
+ atomicWriteFile(target, text);
122
+ return { kind: "stored", relPath: join("snapshots", clientId, opId) };
123
+ }
124
+
125
+ /** Commit the row. Pruning is post-commit and can never fail the append. */
126
+ export function appendOperation(entry: JournalEntry, dir: string = integrationsDir()): void {
127
+ ensureDir(journalPath(dir));
128
+ appendFileSync(journalPath(dir), `${JSON.stringify(entry)}\n`, { encoding: "utf8", mode: 0o600 });
129
+ try {
130
+ const pruned = pruneSnapshots(entry.clientId, dir);
131
+ if (pruned.ok) clearPruneFailure(entry.clientId, dir);
132
+ else markPruneFailure(entry.clientId, pruned.error, dir);
133
+ } catch (error) {
134
+ console.error(`[integrations] post-commit maintenance failed: ${String(error)}`);
135
+ }
136
+ }
137
+
138
+ /** Newest first. A torn final line (crash mid-append) is skipped, not thrown. */
139
+ export function listOperations(
140
+ clientId?: IntegrationClientId,
141
+ limit = 50,
142
+ dir: string = integrationsDir(),
143
+ ): JournalEntry[] {
144
+ let raw: string;
145
+ try {
146
+ raw = readFileSync(journalPath(dir), "utf8");
147
+ } catch {
148
+ return [];
149
+ }
150
+ const rows: JournalEntry[] = [];
151
+ for (const line of raw.split("\n")) {
152
+ if (!line.trim()) continue;
153
+ try {
154
+ const parsed = JSON.parse(line) as JournalEntry;
155
+ if (!clientId || parsed.clientId === clientId) rows.push(parsed);
156
+ } catch {
157
+ // Torn line from an interrupted append; the rest of the log is still good.
158
+ }
159
+ }
160
+ return rows.reverse().slice(0, limit);
161
+ }
162
+
163
+ export function findOperation(opId: string, dir: string = integrationsDir()): JournalEntry | null {
164
+ return listOperations(undefined, Number.MAX_SAFE_INTEGER, dir).find(row => row.opId === opId) ?? null;
165
+ }
166
+
167
+ /** Resolves the tag against what is actually on disk now. */
168
+ export function readSnapshot(
169
+ entry: JournalEntry,
170
+ dir: string = integrationsDir(),
171
+ ): { kind: "none" } | { kind: "stored"; text: string; path: string } | { kind: "expired" } {
172
+ if (entry.snapshot.kind === "none") return { kind: "none" };
173
+ if (entry.snapshot.kind === "expired") return { kind: "expired" };
174
+ // Derive the path from validated identifiers rather than trusting the
175
+ // persisted relPath, then verify containment either way.
176
+ const abs = containedPath(dir, entry.snapshot.relPath);
177
+ if (!existsSync(abs)) return { kind: "expired" };
178
+ return { kind: "stored", text: readFileSync(abs, "utf8"), path: abs };
179
+ }
180
+
181
+ /**
182
+ * Snapshot files retained right now — the witness for `retentionDegraded`.
183
+ *
184
+ * `null` means "cannot inspect", which is NOT the same as zero: reporting an
185
+ * unreadable snapshot directory as a healthy empty one would hide the
186
+ * unbounded, credential-bearing pile this field exists to disclose.
187
+ */
188
+ export function countSnapshots(
189
+ clientId: IntegrationClientId,
190
+ dir: string = integrationsDir(),
191
+ ): number | null {
192
+ try {
193
+ return readdirSync(snapshotDir(clientId, dir)).length;
194
+ } catch (error) {
195
+ return (error as NodeJS.ErrnoException).code === "ENOENT" ? 0 : null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Keep the newest N snapshot files per client; journal rows always survive.
201
+ * Structured rather than throwing or swallowing: a swallowed failure would let
202
+ * credential-bearing snapshots pile up while every operation reported success.
203
+ */
204
+ export function pruneSnapshots(
205
+ clientId: IntegrationClientId,
206
+ dir: string = integrationsDir(),
207
+ ): { ok: true } | { ok: false; error: string } {
208
+ /*
209
+ * Filter to rows that HAVE a snapshot first, then take the newest N.
210
+ *
211
+ * Taking the newest N operations and filtering afterwards counted rows that
212
+ * never stored anything: an apply-to-absent records `snapshot: none`, so a
213
+ * client whose history alternates stored and none kept only half the backups
214
+ * the contract promises. The docs say ten backups per client, and this is
215
+ * the code that has to make that true.
216
+ */
217
+ const keep = new Set(
218
+ listOperations(clientId, Number.MAX_SAFE_INTEGER, dir)
219
+ .filter(row => row.snapshot.kind === "stored")
220
+ .slice(0, SNAPSHOT_RETENTION)
221
+ .map(row => row.opId),
222
+ );
223
+ let names: string[];
224
+ try {
225
+ names = readdirSync(snapshotDir(clientId, dir));
226
+ } catch (error) {
227
+ const code = (error as NodeJS.ErrnoException).code;
228
+ if (code === "ENOENT") return { ok: true };
229
+ return { ok: false, error: String(error) };
230
+ }
231
+ for (const name of names) {
232
+ if (keep.has(name)) continue;
233
+ try {
234
+ rmSync(join(snapshotDir(clientId, dir), name), { force: true });
235
+ } catch (error) {
236
+ return { ok: false, error: String(error) };
237
+ }
238
+ }
239
+ return { ok: true };
240
+ }
241
+
242
+ export interface MaintenanceState {
243
+ pruneFailures: Partial<Record<IntegrationClientId, { at: string; error: string }>>;
244
+ }
245
+
246
+ function maintenancePath(dir: string): string {
247
+ return join(dir, "maintenance.json");
248
+ }
249
+
250
+ /**
251
+ * Validates rather than casts: `{}` parses fine and would leave `pruneFailures`
252
+ * undefined, so the next mark/clear would throw AFTER the journal row was
253
+ * committed — producing the phantom row the write ordering exists to prevent.
254
+ */
255
+ export function readMaintenance(dir: string = integrationsDir()): MaintenanceState {
256
+ try {
257
+ const parsed: unknown = JSON.parse(readFileSync(maintenancePath(dir), "utf8"));
258
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
259
+ const raw = (parsed as { pruneFailures?: unknown }).pruneFailures;
260
+ const failures: MaintenanceState["pruneFailures"] = {};
261
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
262
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
263
+ if (!isIntegrationClientId(key)) continue;
264
+ if (!value || typeof value !== "object") continue;
265
+ const { at, error } = value as { at?: unknown; error?: unknown };
266
+ if (typeof at !== "string" || typeof error !== "string") continue;
267
+ failures[key] = { at, error };
268
+ }
269
+ }
270
+ return { pruneFailures: failures };
271
+ }
272
+ } catch {
273
+ // Absent or corrupt: no pending retries known.
274
+ }
275
+ return { pruneFailures: {} };
276
+ }
277
+
278
+ function writeMaintenance(state: MaintenanceState, dir: string): void {
279
+ try {
280
+ ensureDir(maintenancePath(dir));
281
+ atomicWriteFile(maintenancePath(dir), `${JSON.stringify(state, null, 2)}\n`);
282
+ } catch (error) {
283
+ // The marker is an optimization for scheduling retries. `retentionDegraded`
284
+ // is derived from the snapshot count, so losing it costs a retry, not the
285
+ // claim — a durable promise must not depend on a write that can fail.
286
+ console.error(`[integrations] could not record maintenance state: ${String(error)}`);
287
+ }
288
+ }
289
+
290
+ export function markPruneFailure(
291
+ clientId: IntegrationClientId,
292
+ error: string,
293
+ dir: string = integrationsDir(),
294
+ ): void {
295
+ const state = readMaintenance(dir);
296
+ state.pruneFailures[clientId] = { at: new Date().toISOString(), error };
297
+ writeMaintenance(state, dir);
298
+ }
299
+
300
+ export function clearPruneFailure(
301
+ clientId: IntegrationClientId,
302
+ dir: string = integrationsDir(),
303
+ ): void {
304
+ const state = readMaintenance(dir);
305
+ if (!(clientId in state.pruneFailures)) return;
306
+ delete state.pruneFailures[clientId];
307
+ writeMaintenance(state, dir);
308
+ }
309
+
310
+ /** Re-attempt every marked client. */
311
+ export function retryPendingPrunes(dir: string = integrationsDir()): void {
312
+ for (const clientId of Object.keys(readMaintenance(dir).pruneFailures) as IntegrationClientId[]) {
313
+ if (pruneSnapshots(clientId, dir).ok) clearPruneFailure(clientId, dir);
314
+ }
315
+ }