@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,314 @@
1
+ /**
2
+ * Text serializers for client config documents.
3
+ *
4
+ * Bun 1.3.14 gives us JSON and JSON5 stringify natively, but the two formats a
5
+ * client config actually needs beyond JSON are hand-rendered here:
6
+ *
7
+ * - **TOML** has no `Bun.TOML.stringify` at all.
8
+ * - **YAML** has `Bun.YAML.stringify`, but it emits flow style with no trailing
9
+ * newline (`{a: 1,b: {c: d}}`). That is valid YAML and completely wrong for a
10
+ * file a user opens, so we render block style ourselves.
11
+ *
12
+ * The renderers originally covered only the shallow shapes our builders emit.
13
+ * That was fine while they only ever saw generated documents — but the
14
+ * integration writer parses the USER's whole config and re-serializes it, so a
15
+ * legitimate `null` or a numeric array in a file we never wrote threw straight
16
+ * out of the writer and surfaced as a 500. They now cover the value domains
17
+ * their formats actually allow.
18
+ *
19
+ * What they still refuse is genuinely unrenderable: a function, a symbol, a
20
+ * non-finite number. Those must fail loudly rather than produce bytes a client
21
+ * might misread — and the writer turns the failure into a structured refusal
22
+ * rather than an exception.
23
+ *
24
+ * Design of record: devlog/_fin/260802_client_toggle_api/011_wp1_builders.md.
25
+ */
26
+
27
+ export type ConfigFormat = "json" | "yaml" | "toml" | "json5";
28
+
29
+ /** Thrown when a document holds a value its format cannot represent. */
30
+ export class UnserializableValueError extends Error {}
31
+
32
+ export const FORMAT_MEDIA_TYPE: Record<ConfigFormat, string> = {
33
+ json: "application/json",
34
+ yaml: "application/yaml",
35
+ toml: "application/toml",
36
+ json5: "application/json5",
37
+ };
38
+
39
+ /**
40
+ * Type guard, not a boolean check — the renderers walk `unknown`, so without
41
+ * the predicate every narrowed branch stays `unknown`.
42
+ */
43
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+
47
+ function isYamlScalar(value: unknown): value is string | number | boolean | null {
48
+ return value === null
49
+ || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
50
+ }
51
+
52
+ /**
53
+ * Keep only unambiguous, single-token strings plain. Everything else uses a
54
+ * JSON-compatible double-quoted scalar, which gives deterministic escaping —
55
+ * JSON string escaping is valid YAML 1.2 string escaping.
56
+ */
57
+ function yamlString(value: string): string {
58
+ const plainSafe = value.length > 0
59
+ && value.trim() === value
60
+ && /^[A-Za-z_./][A-Za-z0-9_./-]*$/u.test(value)
61
+ && !/^(?:null|true|false|yes|no|on|off|~|\.nan|[-+]?\.inf)$/iu.test(value);
62
+ return plainSafe ? value : JSON.stringify(value);
63
+ }
64
+
65
+ function yamlScalar(value: string | number | boolean | null): string {
66
+ // `null` renders as the explicit `null` token rather than an empty value:
67
+ // a bare `key:` is also null in YAML, but round-tripping it through other
68
+ // parsers is less predictable than saying so.
69
+ if (value === null) return "null";
70
+ if (typeof value === "string") return yamlString(value);
71
+ if (typeof value === "boolean") return value ? "true" : "false";
72
+ if (Number.isFinite(value)) return String(value);
73
+ throw new UnserializableValueError(`YAML cannot represent the number ${String(value)}`);
74
+ }
75
+
76
+ function yamlEmptyCollection(value: unknown): "[]" | "{}" | undefined {
77
+ if (Array.isArray(value) && value.length === 0) return "[]";
78
+ if (isPlainRecord(value) && Object.keys(value).length === 0) return "{}";
79
+ return undefined;
80
+ }
81
+
82
+ function yamlMapEntryLines(key: string, value: unknown, indent: number): string[] {
83
+ const padding = " ".repeat(indent);
84
+ const renderedKey = yamlString(key);
85
+ if (isYamlScalar(value)) return [`${padding}${renderedKey}: ${yamlScalar(value)}`];
86
+ const empty = yamlEmptyCollection(value);
87
+ if (empty !== undefined) return [`${padding}${renderedKey}: ${empty}`];
88
+ if (Array.isArray(value) || isPlainRecord(value)) {
89
+ return [`${padding}${renderedKey}:`, ...yamlLines(value, indent + 2)];
90
+ }
91
+ throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(key)}: ${describeValue(value)}`);
92
+ }
93
+
94
+ /** A map inside a sequence: the first key rides the dash, the rest indent under it. */
95
+ function yamlArrayMapLines(value: Record<string, unknown>, indent: number): string[] {
96
+ const entries = Object.entries(value);
97
+ const padding = " ".repeat(indent);
98
+ if (entries.length === 0) return [`${padding}- {}`];
99
+
100
+ const [first, ...rest] = entries;
101
+ const [firstKey, firstValue] = first!;
102
+ const renderedFirstKey = yamlString(firstKey);
103
+ const firstEmpty = yamlEmptyCollection(firstValue);
104
+ const lines: string[] = [];
105
+ if (isYamlScalar(firstValue)) {
106
+ lines.push(`${padding}- ${renderedFirstKey}: ${yamlScalar(firstValue)}`);
107
+ } else if (firstEmpty !== undefined) {
108
+ lines.push(`${padding}- ${renderedFirstKey}: ${firstEmpty}`);
109
+ } else if (Array.isArray(firstValue) || isPlainRecord(firstValue)) {
110
+ lines.push(`${padding}- ${renderedFirstKey}:`);
111
+ lines.push(...yamlLines(firstValue, indent + 4));
112
+ } else {
113
+ throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(firstKey)}: ${describeValue(firstValue)}`);
114
+ }
115
+ for (const [key, child] of rest) lines.push(...yamlMapEntryLines(key, child, indent + 2));
116
+ return lines;
117
+ }
118
+
119
+ function yamlLines(value: unknown, indent: number): string[] {
120
+ const padding = " ".repeat(indent);
121
+ if (isYamlScalar(value)) return [`${padding}${yamlScalar(value)}`];
122
+ if (Array.isArray(value)) {
123
+ if (value.length === 0) return [`${padding}[]`];
124
+ return value.flatMap(item => {
125
+ if (isYamlScalar(item)) return [`${padding}- ${yamlScalar(item)}`];
126
+ if (isPlainRecord(item)) return yamlArrayMapLines(item, indent);
127
+ /*
128
+ * A nested sequence is ordinary YAML (`matrix: [[1,2],[3,4]]`). This
129
+ * used to throw a plain Error carrying `String(item)` — untyped, so the
130
+ * writer could not turn it into a refusal and it surfaced as a 500, and
131
+ * carrying the value's CONTENTS into the message, which a config may
132
+ * not want repeated.
133
+ */
134
+ if (Array.isArray(item)) {
135
+ if (item.length === 0) return [`${padding}- []`];
136
+ return [`${padding}-`, ...yamlLines(item, indent + 2)];
137
+ }
138
+ throw new UnserializableValueError(
139
+ `YAML cannot represent this sequence item: ${describeValue(item)}`,
140
+ );
141
+ });
142
+ }
143
+ if (isPlainRecord(value)) {
144
+ const entries = Object.entries(value);
145
+ if (entries.length === 0) return [`${padding}{}`];
146
+ return entries.flatMap(([key, child]) => yamlMapEntryLines(key, child, indent));
147
+ }
148
+ throw new UnserializableValueError(`YAML cannot represent this value: ${describeValue(value)}`);
149
+ }
150
+
151
+ /** Block-style YAML for the shallow shapes we emit. Throws on anything else. */
152
+ export function renderYaml(value: unknown, indent = 0): string {
153
+ if (!Number.isInteger(indent) || indent < 0) {
154
+ throw new Error(`YAML indent must be a non-negative integer: ${String(indent)}`);
155
+ }
156
+ return `${yamlLines(value, indent).join("\n")}\n`;
157
+ }
158
+
159
+ /** A short, safe description for an error message — never the value's contents. */
160
+ function describeValue(value: unknown): string {
161
+ if (value === null) return "null";
162
+ if (Array.isArray(value)) return "an array";
163
+ if (typeof value === "object") return "an object";
164
+ return typeof value;
165
+ }
166
+
167
+ /** TOML basic-string escape. Mirrors `tomlString` in src/grok/inject.ts. */
168
+ export function tomlString(value: string): string {
169
+ return JSON.stringify(value);
170
+ }
171
+
172
+ /** Bare key when safe, basic-string key otherwise (TOML 1.0 §Keys). */
173
+ export function quoteTomlKey(key: string): string {
174
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
175
+ }
176
+
177
+ function tomlScalar(value: unknown): string {
178
+ if (typeof value === "string") return tomlString(value);
179
+ if (typeof value === "boolean") return value ? "true" : "false";
180
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
181
+ /*
182
+ * Arrays of ANY scalar, not just strings. The string-only check was written
183
+ * against our own builder output; a user's config legitimately holds
184
+ * `ports = [1, 2]`, and refusing it threw out of the writer as a 500.
185
+ * TOML permits mixed-type arrays since 1.0, so the elements are rendered
186
+ * individually rather than required to share a type.
187
+ */
188
+ if (Array.isArray(value)) {
189
+ return `[${value.map(item => tomlScalar(item)).join(", ")}]`;
190
+ }
191
+ /*
192
+ * An inline table is valid TOML (`items = [{ x = 1 }]`), and rendering one
193
+ * inline is straightforward — but only where the whole array can be
194
+ * rendered, so this stays a scalar-position renderer rather than silently
195
+ * flattening a table into a string.
196
+ */
197
+ if (isPlainRecord(value)) {
198
+ const entries = Object.entries(value);
199
+ return `{ ${entries.map(([k, v]) => `${quoteTomlKey(k)} = ${tomlScalar(v)}`).join(", ")} }`;
200
+ }
201
+ throw new UnserializableValueError(`TOML cannot represent this value: ${describeValue(value)}`);
202
+ }
203
+
204
+ /**
205
+ * Render `{ [table]: { key: scalar } }` as TOML. Scalars come first, then
206
+ * tables in insertion order, so two calls with the same document produce
207
+ * identical bytes — the stability guarantee `normalizeExportModels` exists to
208
+ * protect.
209
+ */
210
+ export function renderToml(document: Record<string, unknown>, prefix = ""): string {
211
+ const scalars: string[] = [];
212
+ const tables: string[] = [];
213
+ for (const [key, value] of Object.entries(document)) {
214
+ const path = prefix ? `${prefix}.${quoteTomlKey(key)}` : quoteTomlKey(key);
215
+ if (isPlainRecord(value)) {
216
+ tables.push(`[${path}]\n${renderToml(value, path)}`.trimEnd());
217
+ } else {
218
+ scalars.push(`${quoteTomlKey(key)} = ${tomlScalar(value)}`);
219
+ }
220
+ }
221
+ return `${[scalars.join("\n"), tables.join("\n\n")].filter(Boolean).join("\n\n")}\n`;
222
+ }
223
+
224
+ /**
225
+ * Ceiling on container nesting for json documents, shared by the parse-time
226
+ * scanner (config-io.ts) and the serializer walk below. One constant on
227
+ * purpose: the walk must accept every document the scanner admits, or a file
228
+ * the classifier reported as recoverable would refuse at rewrite time. Real
229
+ * configs nest a handful of levels.
230
+ */
231
+ export const MAX_JSON_NESTING = 1000;
232
+
233
+ /** Error messages carry the path to the offending value; keep them readable. */
234
+ function clampPath(path: string): string {
235
+ return path.length > 200 ? `${path.slice(0, 100)}…${path.slice(-100)}` : path;
236
+ }
237
+
238
+ /**
239
+ * JSON.stringify writes a non-finite number as `null` and -0 as `0`; any
240
+ * other finite double round-trips value-exactly (literal-level rounding is
241
+ * the parse-time scanner's concern), so those two are exactly what this walk
242
+ * refuses — refusing more turned a state the classifier had promised as
243
+ * recoverable into a permanent refusal. Documents read from disk are already
244
+ * guarded at parse time, and the writer's merge layer JSON-clones documents —
245
+ * normalizing these values — before serializing, so on the apply/disable path
246
+ * this walk is unreachable for them: it guards the direct serializers
247
+ * (preview/export builders), same posture as the YAML and TOML renderers
248
+ * above, and enforces the nesting ceiling for every json caller before the
249
+ * recursive JSON.stringify can turn depth into a RangeError.
250
+ *
251
+ * Iterative frames instead of recursion or a node stack: depth AND size of
252
+ * the document are inputs under the writer of the config file. Recursion made
253
+ * a deep file a RangeError-500; materializing every node with its path made a
254
+ * wide file allocate a large multiple of its size. Frames keep memory
255
+ * proportional to nesting depth, and path strings exist only for the
256
+ * containers on the current path plus the failing value itself.
257
+ */
258
+ function assertJsonNumbersRoundTrip(document: unknown, rootPath: string): void {
259
+ const refuse = (value: number, path: string): never => {
260
+ throw new UnserializableValueError(Object.is(value, -0)
261
+ ? `JSON cannot rewrite -0 at ${clampPath(path)} without changing it to 0`
262
+ : `JSON cannot rewrite the number at ${clampPath(path)} without changing it to null`);
263
+ };
264
+ if (typeof document === "number" && (!Number.isFinite(document) || Object.is(document, -0))) {
265
+ refuse(document, rootPath);
266
+ }
267
+ type Frame = { container: unknown; keys: string[] | null; index: number; prefix: string };
268
+ const frames: Frame[] = [];
269
+ const pushContainer = (value: unknown, prefix: string) => {
270
+ if (Array.isArray(value)) frames.push({ container: value, keys: null, index: 0, prefix });
271
+ else if (isPlainRecord(value)) frames.push({ container: value, keys: Object.keys(value), index: 0, prefix });
272
+ };
273
+ pushContainer(document, rootPath);
274
+ while (frames.length > 0) {
275
+ const frame = frames[frames.length - 1]!;
276
+ const length = frame.keys ? frame.keys.length : (frame.container as unknown[]).length;
277
+ if (frame.index >= length) { frames.pop(); continue; }
278
+ const i = frame.index;
279
+ frame.index += 1;
280
+ const child = frame.keys
281
+ ? (frame.container as Record<string, unknown>)[frame.keys[i]!]
282
+ : (frame.container as unknown[])[i];
283
+ const childPath = () => frame.keys
284
+ ? (frame.prefix === "$" ? frame.keys[i]! : `${frame.prefix}.${frame.keys[i]!}`)
285
+ : `${frame.prefix}[${i}]`;
286
+ if (typeof child === "number") {
287
+ if (!Number.isFinite(child) || Object.is(child, -0)) refuse(child, childPath());
288
+ continue;
289
+ }
290
+ if (typeof child === "object" && child !== null) {
291
+ if (frames.length >= MAX_JSON_NESTING) {
292
+ throw new UnserializableValueError(
293
+ `the document nests deeper than ${MAX_JSON_NESTING} levels at ${clampPath(childPath())}, which JSON serialization cannot rewrite safely`);
294
+ }
295
+ pushContainer(child, childPath());
296
+ }
297
+ }
298
+ }
299
+
300
+ /** Every serializer returns text ending in exactly one newline. */
301
+ export function serializeDocument(document: unknown, format: ConfigFormat): string {
302
+ switch (format) {
303
+ case "json": {
304
+ assertJsonNumbersRoundTrip(document, "$");
305
+ return `${JSON.stringify(document, null, 2)}\n`;
306
+ }
307
+ case "json5": return `${Bun.JSON5.stringify(document, null, 2)}\n`;
308
+ case "yaml": return renderYaml(document);
309
+ case "toml": {
310
+ if (!isPlainRecord(document)) throw new Error("TOML root must be a table");
311
+ return renderToml(document);
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,361 @@
1
+ /**
2
+ * "What is on disk, and did we put it there?"
3
+ *
4
+ * The classifier is deliberately ordered, and the order is load-bearing: an
5
+ * unreadable file can never be reported as absent, and an edit to a fragment
6
+ * we own can never be reported as ordinary drift. Getting that wrong would let
7
+ * `disable` delete a user's own edits.
8
+ *
9
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §3.
10
+ */
11
+ import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
12
+ import type { OcxConfig } from "../types";
13
+ import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
14
+ import { SNAPSHOT_RETENTION } from "./journal";
15
+ import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
16
+ import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
17
+ import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
18
+
19
+ export type IntegrationState = "absent" | "current" | "stale" | "conflict" | "unsafe";
20
+ export type StateReason =
21
+ | "unparseable"
22
+ | "not-regular-file"
23
+ | "foreign-edit"
24
+ | "unowned-key"
25
+ /** A container we would have to write through holds a non-object value. */
26
+ | "blocked-container"
27
+ /** A path selector we cannot resolve, e.g. a relative OPENCLAW_CONFIG_PATH. */
28
+ | "unresolvable-path";
29
+
30
+ export interface IntegrationStatus {
31
+ clientId: IntegrationClientId;
32
+ state: IntegrationState;
33
+ installed: boolean;
34
+ configPath: string;
35
+ appliedAt?: string;
36
+ lastOpId?: string;
37
+ reason?: StateReason;
38
+ /** Snapshot files retained for this client; -1 when they cannot be inspected. */
39
+ snapshotCount: number;
40
+ /** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
41
+ retentionDegraded: boolean;
42
+ }
43
+
44
+ export function readPath(doc: unknown, path: readonly string[]): unknown {
45
+ let cursor: unknown = doc;
46
+ for (const key of path) {
47
+ if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) return undefined;
48
+ cursor = (cursor as Record<string, unknown>)[key];
49
+ if (cursor === undefined) return undefined;
50
+ }
51
+ return cursor;
52
+ }
53
+
54
+ /** Does the document carry any fragment we would write? */
55
+ export function hasOurFragments(doc: unknown, contribution: ManagedContribution): boolean {
56
+ return contribution.fragments.some(fragment => readPath(doc, fragment.path) !== undefined);
57
+ }
58
+
59
+ /**
60
+ * A container on one of our fragment paths that exists but is NOT an object.
61
+ *
62
+ * `setPath` replaces such a value with `{}` on the way to writing our leaf, so
63
+ * a user whose config held `providers: ["something"]` — legal in their schema,
64
+ * just not ours — lost it to an apply that reported success. The classifier
65
+ * called that document `absent` because our leaf was missing, which authorized
66
+ * the write. Detecting it here turns a silent overwrite into a refusal the
67
+ * user can act on; the snapshot exists, but "we backed up the thing we should
68
+ * not have destroyed" is not the promise this feature makes.
69
+ */
70
+ export function blockedContainerPath(
71
+ doc: unknown,
72
+ contribution: ManagedContribution,
73
+ ): readonly string[] | null {
74
+ for (const fragment of contribution.fragments) {
75
+ let cursor: unknown = doc;
76
+ for (let depth = 0; depth < fragment.path.length - 1; depth += 1) {
77
+ const key = fragment.path[depth]!;
78
+ /*
79
+ * ONLY `undefined` means absent. A missing file parses as `{}`, so an
80
+ * absent prefix reads `undefined` — but a parsed `null` is a value the
81
+ * user's file actually contains, and treating it as absent let a
82
+ * document that is literally `null` be replaced wholesale by a
83
+ * "successful" apply.
84
+ */
85
+ if (cursor === undefined) break;
86
+ // `typeof null === "object"`, so null has to be named explicitly or it
87
+ // walks straight into the dereference below.
88
+ if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
89
+ return fragment.path.slice(0, depth);
90
+ }
91
+ const next = (cursor as Record<string, unknown>)[key];
92
+ if (next === undefined) break;
93
+ if (typeof next !== "object" || next === null || Array.isArray(next)) {
94
+ return fragment.path.slice(0, depth + 1);
95
+ }
96
+ cursor = next;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Fingerprint the recorded fragments as they appear in the document now.
104
+ *
105
+ * The record intentionally names every path we own. Comparing just those
106
+ * values lets another integration or a user add a sibling without blocking a
107
+ * later refresh, while a change inside our block still fails closed.
108
+ */
109
+ function recordedFragmentFingerprint(
110
+ doc: unknown,
111
+ record: OwnershipRecord,
112
+ ): string | null {
113
+ if (
114
+ !Array.isArray(record.fragmentPaths)
115
+ || record.fragmentPaths.length === 0
116
+ || !record.fragmentPaths.every(path => (
117
+ Array.isArray(path)
118
+ && path.length > 0
119
+ && path.every(key => typeof key === "string")
120
+ ))
121
+ ) return null;
122
+ const fragments = [];
123
+ for (const path of record.fragmentPaths) {
124
+ const value = readPath(doc, path);
125
+ if (value === undefined) return null;
126
+ fragments.push({ path, value });
127
+ }
128
+ return fingerprint(canonicalContribution({
129
+ clientId: record.clientId,
130
+ fragments,
131
+ }));
132
+ }
133
+
134
+ /**
135
+ * The two-axis rule: the recorded bytes or fragments prove nobody changed
136
+ * what we may rewrite, and the contribution hash proves our catalog has not
137
+ * moved on. Three classes of client (revising the unconditional whole-file
138
+ * rule of devlog 260802_client_toggle_api/021 §3 for json — #1631):
139
+ * registry-declared source-preserving YAML clients are fragment-scoped because
140
+ * their writers patch only the owned leaf, so the whole-file check is skipped;
141
+ * strict-json clients keep the whole-file check but downgrade a drift with
142
+ * intact owned fragments to `stale`, because a rewrite there can lose only
143
+ * formatting (comments cannot parse, non-round-tripping numbers are refused
144
+ * by the serializer); every comment-capable whole-document serializer (yaml,
145
+ * json5, toml) retains the whole-file fingerprint guard as a hard conflict.
146
+ */
147
+ export function classifyIntegration(input: {
148
+ fileText: string | null;
149
+ fileIsRegular: boolean;
150
+ parsed: unknown | typeof PARSE_FAILED;
151
+ record: OwnershipRecord | null;
152
+ contribution: ManagedContribution;
153
+ /**
154
+ * The file being classified. A record only describes the file it was written
155
+ * for, so this is compared against `record.configPath` before any
156
+ * fingerprint is trusted.
157
+ */
158
+ configPath?: string;
159
+ clientId?: IntegrationClientId;
160
+ }): { state: IntegrationState; reason?: StateReason } {
161
+ if (input.fileText !== null && !input.fileIsRegular) {
162
+ return { state: "unsafe", reason: "not-regular-file" };
163
+ }
164
+ if (input.parsed === PARSE_FAILED) return { state: "unsafe", reason: "unparseable" };
165
+ /*
166
+ * Checked BEFORE `absent`: our leaf is missing in exactly this case, so the
167
+ * absent branch would authorize an apply that replaces the user's value.
168
+ */
169
+ if (blockedContainerPath(input.parsed, input.contribution)) {
170
+ return { state: "unsafe", reason: "blocked-container" };
171
+ }
172
+ if (!hasOurFragments(input.parsed, input.contribution)) return { state: "absent" };
173
+ if (!input.record) return { state: "conflict", reason: "unowned-key" };
174
+ /*
175
+ * A record proves ownership of ONE file. Change HOME, XDG_CONFIG_HOME,
176
+ * HERMES_HOME or KIMI_CODE_HOME and the same client resolves to a different
177
+ * path — whose contents may hash identically because we generate the same
178
+ * bytes. Trusting the fingerprint alone would let a record for path A grant
179
+ * `current` on path B, and the writer resolves the CURRENT path, so disable
180
+ * would then delete fragments from a file this record never owned.
181
+ */
182
+ if (input.clientId !== undefined && input.record.clientId !== input.clientId) {
183
+ return { state: "conflict", reason: "unowned-key" };
184
+ }
185
+ if (input.configPath !== undefined && input.record.configPath !== input.configPath) {
186
+ return { state: "conflict", reason: "unowned-key" };
187
+ }
188
+ const clientId = input.clientId ?? input.record.clientId;
189
+ /*
190
+ * Checked BEFORE file-level drift: an edit INSIDE an owned fragment is a
191
+ * conflict no matter what the rest of the file looks like, so the sibling-
192
+ * edit exemption below can never mask it.
193
+ */
194
+ if (recordedFragmentFingerprint(input.parsed, input.record) !== input.record.blockFingerprint) {
195
+ return { state: "conflict", reason: "foreign-edit" };
196
+ }
197
+ if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
198
+ && fingerprint(input.fileText ?? "") !== input.record.fileFingerprint) {
199
+ /*
200
+ * The file changed since we wrote it, but every fragment we own is still
201
+ * byte-for-byte what we put there — a sibling edit, not tampering. Apply
202
+ * rewrites the WHOLE document, so for comment-capable formats (yaml,
203
+ * json5, toml) it would drop comments the user wrote next to us: fail
204
+ * closed there. Strict JSON cannot carry comments — a commented file
205
+ * never reaches this branch because parsing already failed — so the only
206
+ * possible loss is formatting normalization: everything a rewrite would
207
+ * actually change (numbers that would not round-trip, duplicate members
208
+ * a rewrite would delete) is PARSE_FAILED in parseConfig and classifies
209
+ * as unsafe long before this branch, exactly like comments. Refusing
210
+ * forever over formatting
211
+ * dead-ends the integration on the user's first own config edit (#1631).
212
+ * Report drift instead; a re-apply merges into the parsed document as it
213
+ * stands and re-owns the file. This also lets disable proceed on a
214
+ * drifted file — removal still touches only the recorded fragment paths.
215
+ */
216
+ if (EXPORT_CLIENTS[clientId].format !== "json") {
217
+ return { state: "conflict", reason: "foreign-edit" };
218
+ }
219
+ return { state: "stale" };
220
+ }
221
+ return input.record.blockFingerprint === fingerprint(canonicalContribution(input.contribution))
222
+ ? { state: "current" }
223
+ : { state: "stale" };
224
+ }
225
+
226
+ export interface IntegrationStateInput {
227
+ clientId: IntegrationClientId;
228
+ models: readonly ExportModel[];
229
+ config: OcxConfig;
230
+ port: number;
231
+ env?: NodeJS.ProcessEnv;
232
+ home?: string;
233
+ /** The whole integration state store, bound to one root. */
234
+ store?: IntegrationStateStore;
235
+ io?: IntegrationIO;
236
+ }
237
+
238
+ export function exportContextOf(input: {
239
+ models: readonly ExportModel[];
240
+ config: OcxConfig;
241
+ port: number;
242
+ }): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig } {
243
+ return {
244
+ /*
245
+ * Composed through the SAME helper `ocx export` uses. Interpolating the
246
+ * hostname by hand looked equivalent and was not: `::1` produced
247
+ * `http://::1:10100/v1` and `::` produced `http://:::10100/v1`, neither of
248
+ * which is a URL, and a `0.0.0.0` bind wrote a wildcard address no client
249
+ * can dial. `opencodeProxyBaseUrl` brackets IPv6 and maps wildcards to
250
+ * loopback, and every client we write into deserves the same answer the
251
+ * export command already gives.
252
+ */
253
+ baseUrl: opencodeProxyBaseUrl(input.port, input.config.hostname),
254
+ models: input.models,
255
+ config: input.config,
256
+ };
257
+ }
258
+
259
+ let retriedThisProcess = false;
260
+
261
+ /**
262
+ * Retry pending prunes once per process for the default store, and always for
263
+ * an explicitly supplied one so tests stay order-independent. Never throws: a
264
+ * retry failure is a logged no-op, not a failed read.
265
+ */
266
+ export function retryPendingPrunesOnce(store: IntegrationStateStore): void {
267
+ if (store.root === createIntegrationStateStore().root) {
268
+ if (retriedThisProcess) return;
269
+ retriedThisProcess = true;
270
+ }
271
+ try {
272
+ store.retryPendingPrunes();
273
+ } catch (error) {
274
+ console.error(`[integrations] prune retry failed: ${String(error)}`);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Retention is derived from what is ON DISK, not from the maintenance marker.
280
+ * The marker schedules retries and can itself fail to write; a promise about
281
+ * the user's credential-bearing backups must not depend on that.
282
+ */
283
+ function retentionOf(
284
+ clientId: IntegrationClientId,
285
+ store: IntegrationStateStore,
286
+ ): { snapshotCount: number; retentionDegraded: boolean } {
287
+ const counted = store.countSnapshots(clientId);
288
+ if (counted === null) {
289
+ // Cannot inspect: report degraded with -1 rather than a reassuring zero.
290
+ return { snapshotCount: -1, retentionDegraded: true };
291
+ }
292
+ const marked = store.readMaintenance().pruneFailures[clientId] !== undefined;
293
+ return { snapshotCount: counted, retentionDegraded: marked || counted > SNAPSHOT_RETENTION };
294
+ }
295
+
296
+ /** The ONE reader every surface uses. */
297
+ export function readIntegrationState(input: IntegrationStateInput): IntegrationStatus {
298
+ const store = input.store ?? createIntegrationStateStore();
299
+ retryPendingPrunesOnce(store);
300
+ const io = input.io ?? store.io();
301
+ const spec = INTEGRATION_CLIENTS[input.clientId];
302
+ const exportSpec = EXPORT_CLIENTS[input.clientId];
303
+ const retention = retentionOf(input.clientId, store);
304
+ /*
305
+ * Resolution can refuse — a relative OPENCLAW_* selector names a file whose
306
+ * meaning depends on a working directory we cannot know. The LIST route asks
307
+ * every client for its state, so letting that escape would answer 500 for
308
+ * the whole Integrations page because one client is misconfigured.
309
+ */
310
+ let configPath: string;
311
+ let installed: boolean;
312
+ try {
313
+ configPath = spec.configPath(input.env, input.home);
314
+ installed = io.statKind(spec.detectDir(input.env, input.home)) === "dir";
315
+ } catch (error) {
316
+ if (!(error instanceof ClientPathError)) throw error;
317
+ return {
318
+ clientId: input.clientId,
319
+ state: "unsafe",
320
+ installed: false,
321
+ configPath: "",
322
+ reason: "unresolvable-path",
323
+ ...retention,
324
+ };
325
+ }
326
+
327
+ const target = loadTarget(io, configPath);
328
+ if (!target.ok) {
329
+ return {
330
+ clientId: input.clientId,
331
+ state: "unsafe",
332
+ installed,
333
+ configPath,
334
+ reason: target.why === "read-failed" ? "unparseable" : "not-regular-file",
335
+ ...retention,
336
+ };
337
+ }
338
+
339
+ const parsed = parseConfig(target.before, exportSpec.format);
340
+ const contribution = exportSpec.buildContribution(exportContextOf(input));
341
+ const record = store.readRecords()[input.clientId] ?? null;
342
+ const { state, reason } = classifyIntegration({
343
+ fileText: target.before,
344
+ fileIsRegular: true,
345
+ parsed,
346
+ record,
347
+ contribution,
348
+ configPath,
349
+ clientId: input.clientId,
350
+ });
351
+
352
+ return {
353
+ clientId: input.clientId,
354
+ state,
355
+ installed,
356
+ configPath,
357
+ ...(reason ? { reason } : {}),
358
+ ...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
359
+ ...retention,
360
+ };
361
+ }