@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,715 @@
1
+ /**
2
+ * Apply, disable and restore an opencodex provider block in a client's config.
3
+ *
4
+ * Everything here exists to keep one promise: a toggle can always be undone.
5
+ * That means every mutation snapshots first, writes atomically, and journals
6
+ * what it did — and that a mutation refuses rather than guesses whenever the
7
+ * file is not in a state we can reason about. A disable that deletes work the
8
+ * user did after us would be worse than never shipping the feature.
9
+ *
10
+ * Design of record: devlog/_fin/260802_client_toggle_api/030 and 031.
11
+ */
12
+ import { homedir } from "node:os";
13
+ import { dirname } from "node:path";
14
+ import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../clients/config-export";
15
+ import { isLoopbackHostname } from "../codex/inject";
16
+ import type { OcxConfig } from "../types";
17
+ import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
18
+ import {
19
+ fingerprint,
20
+ canonicalContribution,
21
+ fragmentPathsOf,
22
+ semanticContribution,
23
+ type OwnershipRecord,
24
+ } from "./ownership";
25
+ import {
26
+ protectedContributionFingerprint,
27
+ refreshablePathsOf,
28
+ semanticProtectedContributionFingerprint,
29
+ } from "./ownership-policy";
30
+ import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
31
+ import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
32
+ import { classifyIntegration, exportContextOf } from "./state";
33
+ import type { IntegrationState } from "./state";
34
+ import { serializeDocument, UnserializableValueError } from "./serialize";
35
+ import { ClientPathError } from "../clients/config-export";
36
+ import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
37
+ import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
38
+ import { patchYamlFragmentSource, sourcePrunableYamlContainers } from "./omp-yaml-source";
39
+ import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
40
+
41
+ export type RefusalReason =
42
+ | "not_installed"
43
+ | "conflict"
44
+ | "unsafe"
45
+ | "non_loopback"
46
+ | "drift_requires_confirm"
47
+ | "snapshot_expired"
48
+ | "write_failed";
49
+
50
+ export interface WriteOk {
51
+ ok: true;
52
+ changed: boolean;
53
+ state: IntegrationState;
54
+ clientId: IntegrationClientId;
55
+ opId?: string;
56
+ message: string;
57
+ }
58
+
59
+ export interface WriteRefused {
60
+ ok: false;
61
+ reason: RefusalReason;
62
+ state: IntegrationState;
63
+ clientId: IntegrationClientId;
64
+ message: string;
65
+ /** Absolute path of a recoverable snapshot, when one exists. */
66
+ snapshotPath?: string;
67
+ /** True when compensation itself failed and the file is intermediate. */
68
+ residual?: boolean;
69
+ }
70
+
71
+ export type WriteOutcome = WriteOk | WriteRefused;
72
+
73
+ export interface IntegrationWriteInput {
74
+ clientId: IntegrationClientId;
75
+ models: readonly ExportModel[];
76
+ config: OcxConfig;
77
+ port: number;
78
+ env?: NodeJS.ProcessEnv;
79
+ home?: string;
80
+ store?: IntegrationStateStore;
81
+ io?: IntegrationIO;
82
+ /** Frozen once by the async coordinator; synchronous callers may omit it. */
83
+ resolvedPaths?: { configPath: string; detectDir: string };
84
+ }
85
+
86
+ export interface IntegrationRestoreInput extends IntegrationWriteInput {
87
+ opId: string;
88
+ confirmDrift?: boolean;
89
+ }
90
+
91
+ function refuse(
92
+ clientId: IntegrationClientId,
93
+ reason: RefusalReason,
94
+ state: IntegrationState,
95
+ message: string,
96
+ snapshotPath?: string,
97
+ ): WriteRefused {
98
+ return { ok: false, reason, state, clientId, message, ...(snapshotPath ? { snapshotPath } : {}) };
99
+ }
100
+
101
+ function messageOf(error: unknown): string {
102
+ return error instanceof Error ? error.message : String(error);
103
+ }
104
+
105
+ interface CommitArgs {
106
+ io: IntegrationIO;
107
+ store: IntegrationStateStore;
108
+ clientId: IntegrationClientId;
109
+ configPath: string;
110
+ before: string | null;
111
+ nextText: string | null;
112
+ record: OwnershipRecord | null;
113
+ priorRecord: OwnershipRecord | null;
114
+ entry: JournalEntry;
115
+ state: IntegrationState;
116
+ snapshotPath?: string;
117
+ }
118
+
119
+ /**
120
+ * Client file, then ownership record, then journal row.
121
+ *
122
+ * The row is last on purpose: a record without a row is a thin history, while
123
+ * a row without a record would advertise an operation the classifier cannot
124
+ * corroborate. If either bookkeeping step fails we put the file back AND
125
+ * restore the ownership the operation replaced.
126
+ */
127
+ function commit(args: CommitArgs): WriteOutcome {
128
+ const { io, clientId, configPath } = args;
129
+ try {
130
+ if (args.nextText === null) io.removeFile(configPath);
131
+ else {
132
+ io.mkdirp(dirname(configPath));
133
+ io.writeText(configPath, args.nextText);
134
+ }
135
+ } catch (error) {
136
+ return refuse(clientId, "write_failed", args.state, messageOf(error), args.snapshotPath);
137
+ }
138
+ try {
139
+ if (args.record) io.putRecord(args.record);
140
+ else io.dropRecord(clientId);
141
+ } catch (error) {
142
+ return compensate(args, error, "could not record ownership");
143
+ }
144
+ try {
145
+ io.appendJournal(args.entry);
146
+ } catch (error) {
147
+ return compensate(args, error, "could not append the journal row");
148
+ }
149
+ return {
150
+ ok: true,
151
+ changed: true,
152
+ state: args.state,
153
+ clientId,
154
+ opId: args.entry.opId,
155
+ message: "ok",
156
+ };
157
+ }
158
+
159
+ function compensate(args: CommitArgs, cause: unknown, what: string): WriteRefused {
160
+ const { io, clientId, configPath } = args;
161
+ try {
162
+ if (args.before === null) io.removeFile(configPath);
163
+ else io.writeText(configPath, args.before);
164
+ if (args.priorRecord) io.putRecord(args.priorRecord);
165
+ else io.dropRecord(clientId);
166
+ } catch {
167
+ // Say so. A false "rolled back" is worse than the original error, because
168
+ // the user would stop looking for the file we left half-written.
169
+ return {
170
+ ok: false,
171
+ reason: "write_failed",
172
+ state: args.state,
173
+ clientId,
174
+ residual: true,
175
+ ...(args.snapshotPath ? { snapshotPath: args.snapshotPath } : {}),
176
+ message: `${what}, and the change could not be rolled back. The file or its ownership record is in an intermediate state; the backup is at ${args.snapshotPath ?? "(none)"}.`,
177
+ };
178
+ }
179
+ return refuse(clientId, "write_failed", args.state, `${what}; the change was rolled back. Cause: ${messageOf(cause)}`, args.snapshotPath);
180
+ }
181
+
182
+ function snapshotAbsPath(store: IntegrationStateStore, entry: JournalEntry): string | undefined {
183
+ const snapshot = store.readSnapshot(entry);
184
+ return snapshot.kind === "stored" ? snapshot.path : undefined;
185
+ }
186
+
187
+ function sourcePreservingFragmentValue(
188
+ contribution: ManagedContribution,
189
+ path: readonly string[],
190
+ ): unknown | undefined {
191
+ const fragment = contribution.fragments.find(item => (
192
+ item.path.length === path.length
193
+ && item.path.every((key, index) => key === path[index])
194
+ ));
195
+ return fragment?.value;
196
+ }
197
+
198
+ /** Shared preflight: detect, gate, read, parse and classify. */
199
+ function preflight(input: IntegrationWriteInput) {
200
+ const store = input.store ?? createIntegrationStateStore();
201
+ const io = input.io ?? defaultIntegrationIO(store);
202
+ const clientId = input.clientId;
203
+ const spec = INTEGRATION_CLIENTS[clientId];
204
+ const exportSpec = EXPORT_CLIENTS[clientId];
205
+ /*
206
+ * Resolution itself can refuse: a relative OPENCLAW_* selector is rejected
207
+ * because we cannot know the gateway's working directory. That is a refusal
208
+ * about the user's configuration, not an internal fault, so it must not
209
+ * escape as an exception — the collection route would answer 500 for the
210
+ * whole Integrations page because one client is misconfigured.
211
+ */
212
+ let configPath: string;
213
+ try {
214
+ configPath = input.resolvedPaths?.configPath ?? spec.configPath(input.env, input.home);
215
+ } catch (error) {
216
+ if (!(error instanceof ClientPathError)) throw error;
217
+ return { failed: refuse(clientId, "unsafe", "unsafe", error.message) } as const;
218
+ }
219
+ store.retryPendingPrunes();
220
+
221
+ const target = loadTarget(io, configPath);
222
+ if (!target.ok) {
223
+ return {
224
+ failed: refuse(clientId, "unsafe", "unsafe",
225
+ target.why === "read-failed"
226
+ ? `${configPath} exists but could not be read`
227
+ : `${configPath} is not a regular file`),
228
+ } as const;
229
+ }
230
+ const before = target.before;
231
+ const parsed = parseConfig(before, exportSpec.format);
232
+ if (parsed === PARSE_FAILED) {
233
+ return { failed: refuse(clientId, "unsafe", "unsafe",
234
+ `${configPath} could not be parsed, or holds something opencodex cannot rewrite without changing it (a non-finite number, a large integer or a tiny one a rewrite would round, -0, a duplicate member, or nesting deeper than 1000 levels)`) } as const;
235
+ }
236
+ const contribution = exportSpec.buildContribution(exportContextOf(input));
237
+ // A record proves ownership of the file it was written FOR. Matching only by
238
+ // client id let a record for one home authorize a write to another whose
239
+ // bytes happened to hash the same — which deleted a config we never touched.
240
+ const stored = store.readRecords()[clientId] ?? null;
241
+ const record = stored && stored.clientId === clientId && stored.configPath === configPath
242
+ ? stored
243
+ : null;
244
+ // `configPath`/`clientId` are load-bearing, not decoration: a record proves
245
+ // ownership of ONE file, and the writer mutates whatever path resolves NOW.
246
+ // Without them a record written for another home directory would grant
247
+ // ownership here and disable would delete fragments it never wrote.
248
+ const classified = classifyIntegration({
249
+ fileText: before, fileIsRegular: true, parsed, record, contribution, configPath, clientId,
250
+ });
251
+ return { failed: undefined, store, io, clientId, spec, exportSpec, configPath, before, parsed, contribution, record, classified } as const;
252
+ }
253
+
254
+ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: boolean): WriteOutcome {
255
+ const pre = preflight(input);
256
+ if (pre.failed) return pre.failed;
257
+ const { store, io, clientId, spec, exportSpec, configPath, before, parsed, contribution, record, classified } = pre;
258
+
259
+ if (io.statKind(input.resolvedPaths?.detectDir ?? spec.detectDir(input.env, input.home)) !== "dir") {
260
+ return refuse(clientId, "not_installed", "absent", `${clientId} is not installed`);
261
+ }
262
+ if (isLoopbackOnly(clientId) && !isLoopbackHostname(input.config.hostname)) {
263
+ return refuse(clientId, "non_loopback", classified.state,
264
+ `The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`);
265
+ }
266
+ if (classified.state === "conflict") {
267
+ return refuse(clientId, "conflict", "conflict",
268
+ classified.reason === "foreign-edit"
269
+ ? `${configPath} changed after opencodex wrote it`
270
+ : `${configPath} already contains an opencodex block we did not write`);
271
+ }
272
+ /*
273
+ * `unsafe` from the classifier means the document is not one we may write
274
+ * through — today that is a container on our fragment path holding a
275
+ * non-object value the merge would replace with `{}`. Unreadable and
276
+ * unparseable files are caught earlier in preflight; this branch exists
277
+ * because the classifier can also refuse a file it CAN read.
278
+ */
279
+ if (classified.state === "unsafe") {
280
+ return refuse(clientId, "unsafe", "unsafe",
281
+ classified.reason === "blocked-container"
282
+ ? `${configPath} holds a value where opencodex would have to write a section, so applying would replace it`
283
+ : `${configPath} cannot be changed safely`);
284
+ }
285
+ /*
286
+ * An implicit catalog sync is refresh-only. Keeping this decision inside the
287
+ * writer's one preflight closes the read-then-apply race where a user could
288
+ * remove the managed block after a caller classified it as stale and a
289
+ * normal apply would silently recreate it.
290
+ */
291
+ if (classified.state === "absent" && !allowAbsent) {
292
+ return {
293
+ ok: true,
294
+ changed: false,
295
+ state: "absent",
296
+ clientId,
297
+ message: "managed block is absent; refresh did not reconnect it",
298
+ };
299
+ }
300
+ if (classified.state === "current") {
301
+ return { ok: true, changed: false, state: "current", clientId, message: "already applied" };
302
+ }
303
+
304
+ // A stale refresh drops what the PREVIOUS record owned before merging: a
305
+ // model that left the catalog would otherwise stay behind as an orphan the
306
+ // new record no longer covers, and disable could never remove it.
307
+ /*
308
+ * The previous record's `createdContainers` has to travel with this removal.
309
+ * Without it the refresh leaves our own empty scaffolding behind in `base`,
310
+ * `createdContainerPaths` then sees the container already present and
311
+ * concludes the user owns it, and the replacement record forgets we made it
312
+ * — so a later disable strands it forever.
313
+ */
314
+ const base = classified.state === "stale" && record
315
+ ? removeFragments(parsed, record.fragmentPaths, new Set(record.createdContainers ?? [])).doc
316
+ : parsed;
317
+ // Computed against the document as it stands BEFORE the merge: afterwards
318
+ // every container exists and "did we create this?" is unanswerable.
319
+ const created = createdContainerPaths(base, contribution);
320
+ /*
321
+ * A document can hold a value its own format cannot round-trip through our
322
+ * renderers. That used to throw straight out of the writer and reach the
323
+ * user as a 500 with no path and no advice; it is a refusal like any other,
324
+ * and the file is untouched because this happens before any write.
325
+ */
326
+ const nextDocument = mergeContribution(base, contribution);
327
+ let text: string;
328
+ try {
329
+ if (spec.sourcePreservingYaml && before !== null) {
330
+ const value = sourcePreservingFragmentValue(contribution, spec.sourcePreservingYaml.path);
331
+ const patched = value === undefined
332
+ ? null
333
+ : patchYamlFragmentSource(
334
+ before,
335
+ spec.sourcePreservingYaml.path,
336
+ { kind: "upsert", value },
337
+ nextDocument,
338
+ );
339
+ if (patched === null) {
340
+ return refuse(clientId, "unsafe", "unsafe",
341
+ `${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so it was left alone`);
342
+ }
343
+ text = patched;
344
+ } else {
345
+ text = serializeDocument(nextDocument, exportSpec.format);
346
+ }
347
+ } catch (error) {
348
+ if (!(error instanceof UnserializableValueError)) throw error;
349
+ return refuse(clientId, "unsafe", "unsafe",
350
+ `${configPath} contains something opencodex cannot rewrite safely (${error.message}), so it was left alone`);
351
+ }
352
+
353
+ // Compare-before-commit: someone may have written between classify and now.
354
+ const recheck = io.readText(configPath);
355
+ const rechecked = recheck.kind === "text" ? recheck.text : recheck.kind === "missing" ? null : undefined;
356
+ if (rechecked === undefined || rechecked !== before) {
357
+ return refuse(clientId, "conflict", "conflict", `${configPath} changed while applying`);
358
+ }
359
+
360
+ const opId = newOpId();
361
+ const snapshot = store.captureSnapshot(clientId, opId, before);
362
+ const at = new Date(io.now()).toISOString();
363
+ const entry: JournalEntry = {
364
+ opId, clientId, kind: classified.state === "stale" ? "refresh" : "apply", at, configPath,
365
+ snapshot, resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
366
+ };
367
+ const refreshablePaths = refreshablePathsOf(contribution);
368
+ return commit({
369
+ io, store, clientId, configPath, before, nextText: text, state: "current",
370
+ priorRecord: record,
371
+ record: {
372
+ clientId, configPath, fileFingerprint: fingerprint(text),
373
+ blockFingerprint: fingerprint(canonicalContribution(contribution)),
374
+ semanticBlockFingerprint: fingerprint(semanticContribution(contribution)),
375
+ ...(refreshablePaths.length > 0 ? {
376
+ protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
377
+ semanticProtectedBlockFingerprint: semanticProtectedContributionFingerprint(
378
+ contribution,
379
+ refreshablePaths,
380
+ ),
381
+ refreshablePaths,
382
+ } : {}),
383
+ fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
384
+ appliedAt: at, opId,
385
+ },
386
+ entry,
387
+ snapshotPath: snapshotAbsPath(store, entry),
388
+ });
389
+ }
390
+
391
+ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
392
+ return applyOrRefreshIntegration(input, true);
393
+ }
394
+
395
+ /** Refresh an owned stale block, but never create or reconnect an absent one. */
396
+ export function refreshIntegration(input: IntegrationWriteInput): WriteOutcome {
397
+ return applyOrRefreshIntegration(input, false);
398
+ }
399
+
400
+ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
401
+ const pre = preflight(input);
402
+ if (pre.failed) return pre.failed;
403
+ const { store, io, clientId, spec, exportSpec, configPath, before, parsed, record, classified } = pre;
404
+
405
+ if (classified.state === "absent") {
406
+ return { ok: true, changed: false, state: "absent", clientId, message: "not applied" };
407
+ }
408
+ if (classified.state === "conflict") {
409
+ return refuse(clientId, "conflict", "conflict",
410
+ classified.reason === "foreign-edit"
411
+ ? `${configPath} changed after opencodex wrote it; disabling would discard that edit`
412
+ : `${configPath} contains an opencodex block we did not write`);
413
+ }
414
+ /*
415
+ * `unsafe` reaches here the same way it reaches apply, and the code below
416
+ * dereferences `record` on the assumption that anything past this point is
417
+ * `current` or `stale`. A blocked container has no record, so disable threw
418
+ * a TypeError and the route answered 500 — the GUI locks the switch, but the
419
+ * CLI and direct API callers do not.
420
+ */
421
+ if (classified.state === "unsafe") {
422
+ return refuse(clientId, "unsafe", "unsafe",
423
+ classified.reason === "blocked-container"
424
+ ? `${configPath} holds a value where opencodex would have to read a section, so nothing can be removed safely`
425
+ : `${configPath} cannot be changed safely`);
426
+ }
427
+
428
+ /*
429
+ * current | stale only. What makes the removal safe is the BLOCK
430
+ * fingerprint, not the file fingerprint: the classifier verified the values
431
+ * at the recorded paths are byte-for-byte what we wrote, so removing them
432
+ * cannot take a user edit with them. The file itself may have drifted — a
433
+ * json client classifies a sibling edit as stale (#1631) — which is why the
434
+ * removal runs against the document as parsed NOW, and the re-serialize is
435
+ * value-safe because non-round-tripping numbers were refused at parse time.
436
+ * Source-preserving YAML clients additionally compute which recorded
437
+ * containers are still source-empty before pruning, so a later sibling or
438
+ * comment makes its ancestor user-owned without protecting our leaf.
439
+ */
440
+ const recordedCreated = record!.createdContainers ?? [];
441
+ const prunableCreated = spec.sourcePreservingYaml && before !== null
442
+ ? sourcePrunableYamlContainers(before, spec.sourcePreservingYaml.path, recordedCreated)
443
+ : recordedCreated;
444
+ if (prunableCreated === null) {
445
+ return refuse(clientId, "unsafe", "unsafe",
446
+ `${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so nothing was removed`);
447
+ }
448
+ const { doc, removed } = removeFragments(
449
+ parsed,
450
+ record!.fragmentPaths,
451
+ new Set(prunableCreated),
452
+ );
453
+ if (!removed) {
454
+ return { ok: true, changed: false, state: "absent", clientId, message: "nothing to remove" };
455
+ }
456
+ let text: string;
457
+ try {
458
+ if (spec.sourcePreservingYaml && before !== null) {
459
+ const patched = patchYamlFragmentSource(before, spec.sourcePreservingYaml.path, {
460
+ kind: "remove",
461
+ createdContainers: prunableCreated,
462
+ }, doc);
463
+ if (patched === null) {
464
+ return refuse(clientId, "unsafe", "unsafe",
465
+ `${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so nothing was removed`);
466
+ }
467
+ text = patched;
468
+ } else {
469
+ text = serializeDocument(doc, exportSpec.format);
470
+ }
471
+ } catch (error) {
472
+ if (!(error instanceof UnserializableValueError)) throw error;
473
+ return refuse(clientId, "unsafe", "unsafe",
474
+ `${configPath} contains something opencodex cannot rewrite safely (${error.message}), so nothing was removed`);
475
+ }
476
+
477
+ const recheck = io.readText(configPath);
478
+ const rechecked = recheck.kind === "text" ? recheck.text : recheck.kind === "missing" ? null : undefined;
479
+ if (rechecked === undefined || rechecked !== before) {
480
+ return refuse(clientId, "conflict", "conflict", `${configPath} changed while disabling`);
481
+ }
482
+
483
+ const opId = newOpId();
484
+ const snapshot = store.captureSnapshot(clientId, opId, before);
485
+ const at = new Date(io.now()).toISOString();
486
+ const entry: JournalEntry = {
487
+ opId, clientId, kind: "disable", at, configPath, snapshot,
488
+ resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
489
+ };
490
+ return commit({
491
+ io, store, clientId, configPath, before, nextText: text, state: "absent",
492
+ record: null, priorRecord: record, entry,
493
+ snapshotPath: snapshotAbsPath(store, entry),
494
+ });
495
+ }
496
+
497
+ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome {
498
+ const store = input.store ?? createIntegrationStateStore();
499
+ const io = input.io ?? defaultIntegrationIO(store);
500
+ const entry = store.findOperation(input.opId);
501
+ if (!entry) throw new Error(`unknown operation ${input.opId}`);
502
+ if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
503
+
504
+ const clientId = entry.clientId;
505
+ const resolvedPath = input.resolvedPaths?.configPath
506
+ ?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
507
+ // Restore acts on the path the operation was journaled against. Resolving a
508
+ // different path here would let an operation recorded for one home delete a
509
+ // file in another.
510
+ const configPath = entry.configPath;
511
+ if (resolvedPath !== configPath) {
512
+ return refuse(clientId, "conflict", "conflict",
513
+ `that operation was recorded for ${configPath}, but this client now resolves to ${resolvedPath}`);
514
+ }
515
+ const snapshot = store.readSnapshot(entry);
516
+ if (snapshot.kind === "expired") {
517
+ return refuse(clientId, "snapshot_expired", "absent", "that backup has expired");
518
+ }
519
+ const backupHint = snapshot.kind === "stored" ? snapshot.path : undefined;
520
+
521
+ const target = loadTarget(io, configPath);
522
+ if (!target.ok) {
523
+ return refuse(clientId, "unsafe", "unsafe",
524
+ target.why === "read-failed"
525
+ ? `${configPath} exists but could not be read; the backup is at ${backupHint ?? "(none)"}`
526
+ : `${configPath} is not a regular file; the backup is at ${backupHint ?? "(none)"}`,
527
+ backupHint);
528
+ }
529
+ const current = target.before;
530
+
531
+ /*
532
+ * Drift: the file changed after the operation we are undoing.
533
+ *
534
+ * Through the shared matcher, because `fingerprint(current ?? "")` treated a
535
+ * MISSING file as an empty one — so restoring an operation whose result was
536
+ * absence, with the file still absent, read as drift and demanded a
537
+ * confirmation for edits nobody had made. The journal meanwhile offered the
538
+ * same row as Undo.
539
+ */
540
+ if (!matchesOperationResult(entry, current) && !input.confirmDrift) {
541
+ return refuse(clientId, "drift_requires_confirm", "conflict",
542
+ "this file changed after that operation; confirm to replace it (the current version is backed up first)");
543
+ }
544
+
545
+ // Restore is itself journaled and itself undoable: snapshot the CURRENT file
546
+ // first, so a confirmed drift-restore never destroys the newer edits.
547
+ const opId = newOpId();
548
+ const preSnapshot = store.captureSnapshot(clientId, opId, current);
549
+ const restoredText = snapshot.kind === "none" ? null : snapshot.text;
550
+
551
+ // Provenance is RESTORED, never re-derived: `priorRecord` described these
552
+ // exact bytes when the snapshot was taken. Re-deriving it from the file would
553
+ // mean guessing which entries are ours, and a wrong guess deletes a user's.
554
+ const restoredRecord = entry.priorRecord;
555
+ const fresh = EXPORT_CLIENTS[clientId].buildContribution(exportContextOf(input));
556
+ /*
557
+ * Does the restored record actually describe the restored bytes?
558
+ *
559
+ * It usually does — `priorRecord` was written for exactly this snapshot. But
560
+ * a CONFIRMED drift-restore snapshots the user's edited file first, and
561
+ * undoing that restore puts those edited bytes back while carrying a record
562
+ * that describes what opencodex had written. Overwriting the record's
563
+ * fingerprint with the restored bytes then laundered a foreign edit into
564
+ * owned content: the state read `current`, and a later disable deleted
565
+ * fields the user had added by hand.
566
+ */
567
+ const restoredFingerprint = restoredText === null ? "" : fingerprint(restoredText);
568
+ const recordDescribesBytes = restoredRecord !== null
569
+ && restoredRecord.fileFingerprint === restoredFingerprint;
570
+ const state: IntegrationState = restoredRecord === null
571
+ ? (restoredText === null ? "absent" : "conflict")
572
+ : !recordDescribesBytes
573
+ ? "conflict"
574
+ : (
575
+ restoredRecord.semanticBlockFingerprint === fingerprint(semanticContribution(fresh))
576
+ || restoredRecord.blockFingerprint === fingerprint(canonicalContribution(fresh))
577
+ )
578
+ ? "current"
579
+ : "stale";
580
+
581
+ const at = new Date(io.now()).toISOString();
582
+ const priorRecord = store.readRecords()[clientId] ?? null;
583
+ const restoreEntry: JournalEntry = {
584
+ opId, clientId, kind: "restore", at, configPath, snapshot: preSnapshot,
585
+ resultFingerprint: restoredText === null ? "" : fingerprint(restoredText),
586
+ resultAbsent: restoredText === null,
587
+ priorRecord,
588
+ };
589
+ return commit({
590
+ io, store, clientId, configPath, before: current, nextText: restoredText, state,
591
+ priorRecord,
592
+ /*
593
+ * Keep the record's ORIGINAL `fileFingerprint`. Restoring bytes it does not
594
+ * describe leaves the classifier reading `conflict`, which is the honest
595
+ * answer — we are no longer sure the block on disk is ours, and refusing
596
+ * is what stops a disable from deleting the user's edit.
597
+ */
598
+ record: restoredRecord === null ? null : {
599
+ ...restoredRecord,
600
+ appliedAt: at,
601
+ opId,
602
+ },
603
+ entry: restoreEntry,
604
+ snapshotPath: snapshotAbsPath(store, restoreEntry),
605
+ });
606
+ }
607
+
608
+ export interface CoordinatedIntegrationOptions {
609
+ lockSeams?: IntegrationWriterLockSeams;
610
+ }
611
+
612
+ /** Freeze all mutable resolution seams before the first lock await. */
613
+ type FrozenIntegrationInput = IntegrationWriteInput & {
614
+ store: IntegrationStateStore;
615
+ io: IntegrationIO;
616
+ env: NodeJS.ProcessEnv;
617
+ home: string;
618
+ resolvedPaths: { configPath: string; detectDir: string };
619
+ };
620
+
621
+ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegrationInput {
622
+ const env = { ...(input.env ?? process.env) };
623
+ const home = input.home ?? homedir();
624
+ const store = input.store ?? createIntegrationStateStore();
625
+ const io = input.io ?? defaultIntegrationIO(store);
626
+ const spec = INTEGRATION_CLIENTS[input.clientId];
627
+ const resolvedPaths = {
628
+ configPath: spec.configPath(env, home),
629
+ detectDir: spec.detectDir(env, home),
630
+ };
631
+ return { ...input, env, home, store, io, resolvedPaths };
632
+ }
633
+
634
+ function tryFreezeIntegrationInput(input: IntegrationWriteInput):
635
+ | { ok: true; value: FrozenIntegrationInput }
636
+ | { ok: false; refusal: WriteRefused } {
637
+ try {
638
+ return { ok: true, value: freezeIntegrationInput(input) };
639
+ } catch (error) {
640
+ if (!(error instanceof ClientPathError)) throw error;
641
+ return {
642
+ ok: false,
643
+ refusal: refuse(input.clientId, "unsafe", "unsafe", error.message),
644
+ };
645
+ }
646
+ }
647
+
648
+ async function coordinatedWrite(
649
+ input: IntegrationWriteInput,
650
+ operation: (frozen: IntegrationWriteInput) => WriteOutcome,
651
+ options?: CoordinatedIntegrationOptions,
652
+ ): Promise<WriteOutcome> {
653
+ const prepared = tryFreezeIntegrationInput(input);
654
+ if (!prepared.ok) return prepared.refusal;
655
+ const frozen = prepared.value;
656
+ const spec = INTEGRATION_CLIENTS[frozen.clientId];
657
+ if (!spec.writerLock) return operation(frozen);
658
+
659
+ // An absent client home is not created merely to acquire a sibling lock.
660
+ if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
661
+ return operation(frozen);
662
+ }
663
+ return withIntegrationWriterLock(
664
+ frozen.resolvedPaths.configPath,
665
+ async () => operation(frozen),
666
+ options?.lockSeams,
667
+ spec.writerLock.suffix,
668
+ );
669
+ }
670
+
671
+ export function applyIntegrationCoordinated(
672
+ input: IntegrationWriteInput,
673
+ options?: CoordinatedIntegrationOptions,
674
+ ): Promise<WriteOutcome> {
675
+ return coordinatedWrite(input, applyIntegration, options);
676
+ }
677
+
678
+ export function refreshIntegrationCoordinated(
679
+ input: IntegrationWriteInput,
680
+ options?: CoordinatedIntegrationOptions,
681
+ ): Promise<WriteOutcome> {
682
+ return coordinatedWrite(input, refreshIntegration, options);
683
+ }
684
+
685
+ export function disableIntegrationCoordinated(
686
+ input: IntegrationWriteInput,
687
+ options?: CoordinatedIntegrationOptions,
688
+ ): Promise<WriteOutcome> {
689
+ return coordinatedWrite(input, disableIntegration, options);
690
+ }
691
+
692
+ export async function restoreIntegrationCoordinated(
693
+ input: IntegrationRestoreInput,
694
+ options?: CoordinatedIntegrationOptions,
695
+ ): Promise<WriteOutcome> {
696
+ const prepared = tryFreezeIntegrationInput(input);
697
+ if (!prepared.ok) return prepared.refusal;
698
+ const frozen = prepared.value;
699
+ const spec = INTEGRATION_CLIENTS[frozen.clientId];
700
+ if (!spec.writerLock) return restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
701
+ if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
702
+ return refuse(
703
+ frozen.clientId,
704
+ "unsafe",
705
+ "unsafe",
706
+ `${frozen.resolvedPaths.detectDir} is missing; restore will not create the client home`,
707
+ );
708
+ }
709
+ return withIntegrationWriterLock(
710
+ frozen.resolvedPaths.configPath,
711
+ async () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift }),
712
+ options?.lockSeams,
713
+ spec.writerLock.suffix,
714
+ );
715
+ }