airaknit 1.1.2-rc.9

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 (440) hide show
  1. package/LICENSE +84 -0
  2. package/README.md +202 -0
  3. package/bin/airaknit +9 -0
  4. package/bin/airaknit-project +14 -0
  5. package/bin/kanna +9 -0
  6. package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
  7. package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
  8. package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
  9. package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
  10. package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
  11. package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
  12. package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
  13. package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
  14. package/dist/client/assets/arc-B07zg7ol.js +1 -0
  15. package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
  16. package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
  17. package/dist/client/assets/array-BGFCBI0e.js +1 -0
  18. package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
  19. package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
  20. package/dist/client/assets/button-DO50qOGv.js +1 -0
  21. package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
  22. package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
  23. package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
  24. package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
  25. package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
  26. package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
  27. package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
  28. package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
  29. package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
  30. package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
  31. package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
  32. package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
  33. package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
  34. package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
  35. package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
  36. package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
  37. package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
  38. package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
  39. package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
  40. package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
  41. package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
  42. package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
  43. package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
  44. package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
  45. package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
  46. package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
  47. package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
  48. package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
  49. package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
  50. package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
  51. package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
  52. package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
  53. package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
  54. package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
  55. package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
  56. package/dist/client/assets/client-BrKWI4CM.js +1 -0
  57. package/dist/client/assets/client-CGgNRU9w.js +1 -0
  58. package/dist/client/assets/client-DMSLRzg9.js +6 -0
  59. package/dist/client/assets/clone-DWcL7whJ.js +1 -0
  60. package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
  61. package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
  62. package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
  63. package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
  64. package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
  65. package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
  66. package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
  67. package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
  68. package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
  69. package/dist/client/assets/dist-CuB4kiSK.js +1 -0
  70. package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
  71. package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
  72. package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
  73. package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
  74. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
  75. package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
  76. package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
  77. package/dist/client/assets/index-CetCiuqP.js +105 -0
  78. package/dist/client/assets/index-N29Mip7A.css +1 -0
  79. package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
  80. package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
  81. package/dist/client/assets/init-B8gtcn7T.js +1 -0
  82. package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
  83. package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
  84. package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
  85. package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
  86. package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
  87. package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
  88. package/dist/client/assets/katex-B94qP8b6.js +265 -0
  89. package/dist/client/assets/lib--QVjyxmL.js +29 -0
  90. package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
  91. package/dist/client/assets/line-DCrYfLBn.js +1 -0
  92. package/dist/client/assets/linear-_4upLmeo.js +1 -0
  93. package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
  94. package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
  95. package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
  96. package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
  97. package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
  98. package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
  99. package/dist/client/assets/path-DZF-JdEe.js +1 -0
  100. package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
  101. package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
  102. package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
  103. package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
  104. package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
  105. package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
  106. package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
  107. package/dist/client/assets/react-CClhXMB2.js +1 -0
  108. package/dist/client/assets/react-Dd6D81m0.js +1 -0
  109. package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
  110. package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
  111. package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
  112. package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
  113. package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
  114. package/dist/client/assets/src-DeTlMJAU.js +1 -0
  115. package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
  116. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
  117. package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
  118. package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
  119. package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
  120. package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
  121. package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
  122. package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
  123. package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
  124. package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
  125. package/dist/client/favicon.png +0 -0
  126. package/dist/client/favicon.svg +15 -0
  127. package/dist/client/fonts/body-medium.woff2 +0 -0
  128. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  129. package/dist/client/fonts/body-regular.woff2 +0 -0
  130. package/dist/client/fonts/body-semibold.woff2 +0 -0
  131. package/dist/client/index.html +31 -0
  132. package/dist/client/manifest.webmanifest +12 -0
  133. package/dist/client/sw.js +32 -0
  134. package/package.json +122 -0
  135. package/src/nats/auth-callout/callout-config.test.ts +93 -0
  136. package/src/nats/auth-callout/callout-config.ts +109 -0
  137. package/src/nats/auth-callout/callout.integration.test.ts +332 -0
  138. package/src/nats/auth-callout/keys.ts +103 -0
  139. package/src/nats/auth-callout/responder.ts +241 -0
  140. package/src/nats/auth-callout/scope-policy.test.ts +159 -0
  141. package/src/nats/auth-callout/scope-policy.ts +210 -0
  142. package/src/nats/auth-callout/token.test.ts +163 -0
  143. package/src/nats/auth-callout/token.ts +157 -0
  144. package/src/nats/nats-daemon-callout.ts +194 -0
  145. package/src/nats/nats-daemon.test.ts +77 -0
  146. package/src/nats/nats-daemon.ts +50 -0
  147. package/src/nats/nats-token.test.ts +61 -0
  148. package/src/nats/nats-token.ts +59 -0
  149. package/src/runner/coordination-mcp-integration.test.ts +134 -0
  150. package/src/runner/nats-coordination-client.test.ts +49 -0
  151. package/src/runner/nats-coordination-client.ts +94 -0
  152. package/src/runner/runner-agent.test.ts +469 -0
  153. package/src/runner/runner-agent.ts +453 -0
  154. package/src/runner/runner-credential.test.ts +93 -0
  155. package/src/runner/runner-credential.ts +82 -0
  156. package/src/runner/runner-nats.test.ts +495 -0
  157. package/src/runner/runner-nats.ts +323 -0
  158. package/src/runner/runner-pair.test.ts +107 -0
  159. package/src/runner/runner-pair.ts +81 -0
  160. package/src/runner/runner.test.ts +135 -0
  161. package/src/runner/runner.ts +212 -0
  162. package/src/runner/turn-factories.test.ts +97 -0
  163. package/src/runner/turn-factories.ts +475 -0
  164. package/src/server/agent-config-journey.test.ts +106 -0
  165. package/src/server/agent.ts +8 -0
  166. package/src/server/auto-continue/auth-error-detector.ts +66 -0
  167. package/src/server/auto-continue/limit-detector.ts +194 -0
  168. package/src/server/bm25.test.ts +92 -0
  169. package/src/server/bm25.ts +101 -0
  170. package/src/server/chat-events-jetstream.test.ts +135 -0
  171. package/src/server/claude-harness.ts +360 -0
  172. package/src/server/claude-pty/agent-normalizers.ts +309 -0
  173. package/src/server/claude-pty/auth.test.ts +38 -0
  174. package/src/server/claude-pty/auth.ts +32 -0
  175. package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
  176. package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
  177. package/src/server/claude-pty/driver.test.ts +902 -0
  178. package/src/server/claude-pty/driver.ts +807 -0
  179. package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
  180. package/src/server/claude-pty/jsonl-path.test.ts +114 -0
  181. package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
  182. package/src/server/claude-pty/jsonl-to-event.ts +174 -0
  183. package/src/server/claude-pty/output-ring.test.ts +35 -0
  184. package/src/server/claude-pty/output-ring.ts +25 -0
  185. package/src/server/claude-pty/parity-matrix.test.ts +227 -0
  186. package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
  187. package/src/server/claude-pty/pid-registry.test.ts +122 -0
  188. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
  189. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
  190. package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
  191. package/src/server/claude-pty/pty-instance-registry.ts +166 -0
  192. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
  193. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
  194. package/src/server/claude-pty/pty-process.adapter.ts +66 -0
  195. package/src/server/claude-pty/pty-process.test.ts +49 -0
  196. package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
  197. package/src/server/claude-pty/resolve-binary.test.ts +118 -0
  198. package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
  199. package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
  200. package/src/server/claude-pty/settings-writer.test.ts +22 -0
  201. package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
  202. package/src/server/claude-pty/smoke-test.test.ts +191 -0
  203. package/src/server/claude-pty/smoke-test.ts +185 -0
  204. package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
  205. package/src/server/claude-pty/tool-callback.ts +274 -0
  206. package/src/server/claude-pty/tui-control.test.ts +272 -0
  207. package/src/server/claude-pty/tui-control.ts +182 -0
  208. package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
  209. package/src/server/claude-pty/tui-source.adapter.ts +343 -0
  210. package/src/server/claude-pty/tunnel-gateway.ts +12 -0
  211. package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
  212. package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
  213. package/src/server/claude-pty-mcp/history-primer.ts +90 -0
  214. package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
  215. package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
  216. package/src/server/claude-pty-mcp/mcp.ts +412 -0
  217. package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
  218. package/src/server/claude-pty-mcp/paths.ts +24 -0
  219. package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
  220. package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
  221. package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
  222. package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
  223. package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
  224. package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
  225. package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
  226. package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
  227. package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
  228. package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
  229. package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
  230. package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
  231. package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
  232. package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
  233. package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
  234. package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
  235. package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
  236. package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
  237. package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
  238. package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
  239. package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
  240. package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
  241. package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
  242. package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
  243. package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
  244. package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
  245. package/src/server/claude-pty-mcp/uploads.ts +38 -0
  246. package/src/server/claude-turn.test.ts +176 -0
  247. package/src/server/cli-runtime.test.ts +456 -0
  248. package/src/server/cli-runtime.ts +374 -0
  249. package/src/server/cli-supervisor.ts +81 -0
  250. package/src/server/cli.ts +78 -0
  251. package/src/server/client-log-forwarder.test.ts +74 -0
  252. package/src/server/client-log-forwarder.ts +75 -0
  253. package/src/server/codex-app-server-protocol.ts +449 -0
  254. package/src/server/codex-app-server.test.ts +2990 -0
  255. package/src/server/codex-app-server.ts +1713 -0
  256. package/src/server/coordination-integration.test.ts +63 -0
  257. package/src/server/coordination-mcp.test.ts +149 -0
  258. package/src/server/coordination-mcp.ts +197 -0
  259. package/src/server/delegation-coordinator.test.ts +675 -0
  260. package/src/server/delegation-coordinator.ts +454 -0
  261. package/src/server/discovery.test.ts +211 -0
  262. package/src/server/discovery.ts +301 -0
  263. package/src/server/event-store-agent-config.test.ts +124 -0
  264. package/src/server/event-store-coordination.test.ts +149 -0
  265. package/src/server/event-store-profile.test.ts +132 -0
  266. package/src/server/event-store-repo.test.ts +154 -0
  267. package/src/server/event-store-runner-team.test.ts +104 -0
  268. package/src/server/event-store.test.ts +342 -0
  269. package/src/server/event-store.ts +2208 -0
  270. package/src/server/events.ts +379 -0
  271. package/src/server/extension-router.test.ts +183 -0
  272. package/src/server/extension-router.ts +114 -0
  273. package/src/server/extensions/agents/server.test.ts +191 -0
  274. package/src/server/extensions/agents/server.ts +108 -0
  275. package/src/server/extensions/c3/server.test.ts +284 -0
  276. package/src/server/extensions/c3/server.ts +212 -0
  277. package/src/server/extensions/code/server.test.ts +200 -0
  278. package/src/server/extensions/code/server.ts +150 -0
  279. package/src/server/extensions.config.ts +10 -0
  280. package/src/server/external-open.ts +69 -0
  281. package/src/server/generate-fork-context.ts +58 -0
  282. package/src/server/generate-merge-context.test.ts +290 -0
  283. package/src/server/generate-merge-context.ts +141 -0
  284. package/src/server/generate-title.ts +36 -0
  285. package/src/server/git-clone-policy.test.ts +138 -0
  286. package/src/server/git-clone-policy.ts +27 -0
  287. package/src/server/harness-types.ts +1 -0
  288. package/src/server/journey-verification.test.ts +640 -0
  289. package/src/server/journey-verification.ts +195 -0
  290. package/src/server/machine-name.ts +22 -0
  291. package/src/server/nats-auth.test.ts +92 -0
  292. package/src/server/nats-auth.ts +6 -0
  293. package/src/server/nats-bind-guard.test.ts +71 -0
  294. package/src/server/nats-bind-guard.ts +42 -0
  295. package/src/server/nats-bridge.test.ts +141 -0
  296. package/src/server/nats-bridge.ts +111 -0
  297. package/src/server/nats-connector.test.ts +56 -0
  298. package/src/server/nats-connector.ts +71 -0
  299. package/src/server/nats-daemon-manager.test.ts +76 -0
  300. package/src/server/nats-daemon-manager.ts +174 -0
  301. package/src/server/nats-publisher.test.ts +356 -0
  302. package/src/server/nats-publisher.ts +271 -0
  303. package/src/server/nats-responders.test.ts +1018 -0
  304. package/src/server/nats-responders.ts +925 -0
  305. package/src/server/nats-streams.test.ts +112 -0
  306. package/src/server/nats-streams.ts +129 -0
  307. package/src/server/oauth-pool/oauth-responders.ts +185 -0
  308. package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
  309. package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
  310. package/src/server/orchestration.test.ts +1063 -0
  311. package/src/server/orchestration.ts +716 -0
  312. package/src/server/pairing-endpoints.test.ts +171 -0
  313. package/src/server/pairing-store.test.ts +154 -0
  314. package/src/server/pairing-store.ts +124 -0
  315. package/src/server/paths.ts +27 -0
  316. package/src/server/pr3-liveness.test.ts +252 -0
  317. package/src/server/process-utils.ts +10 -0
  318. package/src/server/project-cli.ts +180 -0
  319. package/src/server/provider-catalog.test.ts +177 -0
  320. package/src/server/provider-catalog.ts +146 -0
  321. package/src/server/pty-responders.ts +345 -0
  322. package/src/server/push-notifications.test.ts +234 -0
  323. package/src/server/push-notifications.ts +188 -0
  324. package/src/server/quick-response.test.ts +196 -0
  325. package/src/server/quick-response.ts +154 -0
  326. package/src/server/read-models-agent-config.test.ts +59 -0
  327. package/src/server/read-models-coordination.test.ts +69 -0
  328. package/src/server/read-models.test.ts +332 -0
  329. package/src/server/read-models.ts +258 -0
  330. package/src/server/repo-journey.test.ts +96 -0
  331. package/src/server/repo-manager.test.ts +118 -0
  332. package/src/server/repo-manager.ts +97 -0
  333. package/src/server/repo-status.test.ts +54 -0
  334. package/src/server/repo-status.ts +82 -0
  335. package/src/server/restart.test.ts +27 -0
  336. package/src/server/restart.ts +30 -0
  337. package/src/server/runner-incompatible-gate.test.ts +383 -0
  338. package/src/server/runner-manager.test.ts +72 -0
  339. package/src/server/runner-manager.ts +312 -0
  340. package/src/server/runner-pairing-urls.test.ts +59 -0
  341. package/src/server/runner-pairing-urls.ts +67 -0
  342. package/src/server/runner-proxy.test.ts +456 -0
  343. package/src/server/runner-proxy.ts +494 -0
  344. package/src/server/runner-router.test.ts +429 -0
  345. package/src/server/runner-router.ts +212 -0
  346. package/src/server/runner-routing.test.ts +584 -0
  347. package/src/server/runtime-registry.test.ts +436 -0
  348. package/src/server/runtime-registry.ts +307 -0
  349. package/src/server/sandbox-health.test.ts +127 -0
  350. package/src/server/sandbox-health.ts +94 -0
  351. package/src/server/sandbox-journey.test.ts +232 -0
  352. package/src/server/sandbox-manager.test.ts +146 -0
  353. package/src/server/sandbox-manager.ts +159 -0
  354. package/src/server/server.test.ts +287 -0
  355. package/src/server/server.ts +1108 -0
  356. package/src/server/session-discovery.test.ts +129 -0
  357. package/src/server/session-discovery.ts +475 -0
  358. package/src/server/session-index.test.ts +362 -0
  359. package/src/server/session-index.ts +119 -0
  360. package/src/server/session-seed.ts +288 -0
  361. package/src/server/share.test.ts +108 -0
  362. package/src/server/share.ts +113 -0
  363. package/src/server/skill-discovery.test.ts +201 -0
  364. package/src/server/skill-discovery.ts +77 -0
  365. package/src/server/storage/test-helpers.ts +67 -0
  366. package/src/server/terminal-manager.test.ts +309 -0
  367. package/src/server/terminal-manager.ts +354 -0
  368. package/src/server/transcript-consumer.test.ts +339 -0
  369. package/src/server/transcript-consumer.ts +162 -0
  370. package/src/server/transcript-search.test.ts +193 -0
  371. package/src/server/transcript-search.ts +83 -0
  372. package/src/server/transcript-utils.ts +52 -0
  373. package/src/server/update-manager.test.ts +107 -0
  374. package/src/server/update-manager.ts +230 -0
  375. package/src/server/workflow-engine.test.ts +251 -0
  376. package/src/server/workflow-engine.ts +169 -0
  377. package/src/server/workflow-mcp.ts +49 -0
  378. package/src/server/workflow-store.test.ts +155 -0
  379. package/src/server/workflow-store.ts +139 -0
  380. package/src/server/workspace-agent-integration.test.ts +167 -0
  381. package/src/server/workspace-agent-routes.test.ts +127 -0
  382. package/src/server/workspace-agent-routes.ts +89 -0
  383. package/src/server/workspace-agent.test.ts +103 -0
  384. package/src/server/workspace-agent.ts +102 -0
  385. package/src/server/workspace-cli.test.ts +79 -0
  386. package/src/server/workspace-config-manager.test.ts +109 -0
  387. package/src/server/workspace-config-manager.ts +83 -0
  388. package/src/server/workspace-directory-policy.test.ts +109 -0
  389. package/src/server/workspace-directory-policy.ts +56 -0
  390. package/src/shared/agent-config-types.ts +25 -0
  391. package/src/shared/branding.test.ts +42 -0
  392. package/src/shared/branding.ts +54 -0
  393. package/src/shared/compression.test.ts +85 -0
  394. package/src/shared/compression.ts +42 -0
  395. package/src/shared/coordination-store.test.ts +24 -0
  396. package/src/shared/coordination-store.ts +26 -0
  397. package/src/shared/dev-ports.test.ts +84 -0
  398. package/src/shared/dev-ports.ts +100 -0
  399. package/src/shared/extension-types.ts +45 -0
  400. package/src/shared/fork-presets.ts +54 -0
  401. package/src/shared/harness-types.test.ts +15 -0
  402. package/src/shared/harness-types.ts +21 -0
  403. package/src/shared/log-sink.test.ts +112 -0
  404. package/src/shared/log-sink.ts +185 -0
  405. package/src/shared/mention-pattern.ts +7 -0
  406. package/src/shared/merge-presets.ts +41 -0
  407. package/src/shared/nats-subjects.test.ts +61 -0
  408. package/src/shared/nats-subjects.ts +131 -0
  409. package/src/shared/permission-policy.ts +136 -0
  410. package/src/shared/ports.ts +3 -0
  411. package/src/shared/preset-types.ts +15 -0
  412. package/src/shared/profile-types.ts +49 -0
  413. package/src/shared/projectFileUrl.ts +36 -0
  414. package/src/shared/protocol.ts +153 -0
  415. package/src/shared/pty-instance.ts +43 -0
  416. package/src/shared/puggy/diagnostics/result.ts +18 -0
  417. package/src/shared/puggy/expressions/evaluate.ts +292 -0
  418. package/src/shared/puggy/index.test.ts +101 -0
  419. package/src/shared/puggy/index.ts +69 -0
  420. package/src/shared/puggy/parser/ast.ts +110 -0
  421. package/src/shared/puggy/parser/parser.ts +624 -0
  422. package/src/shared/puggy/renderer/html.ts +447 -0
  423. package/src/shared/runner-protocol.test.ts +277 -0
  424. package/src/shared/runner-protocol.ts +210 -0
  425. package/src/shared/runner-team-types.ts +73 -0
  426. package/src/shared/runtime-types.ts +48 -0
  427. package/src/shared/sandbox-types.ts +53 -0
  428. package/src/shared/tailwind-build.test.ts +12 -0
  429. package/src/shared/tinkaria-system-prompt.ts +101 -0
  430. package/src/shared/tools.test.ts +335 -0
  431. package/src/shared/tools.ts +397 -0
  432. package/src/shared/transcript-entries.ts +27 -0
  433. package/src/shared/transcript-render.test.ts +225 -0
  434. package/src/shared/transcript-render.ts +467 -0
  435. package/src/shared/types.ts +1031 -0
  436. package/src/shared/vite-config.test.ts +47 -0
  437. package/src/shared/web-context.test.ts +110 -0
  438. package/src/shared/web-context.ts +76 -0
  439. package/src/shared/workflow-types.ts +51 -0
  440. package/src/shared/workspace-types.ts +100 -0
@@ -0,0 +1,1108 @@
1
+ import path from "node:path"
2
+ import { APP_NAME, getRuntimeProfile, LOG_PREFIX } from "../shared/branding"
3
+ import { EventStore } from "./event-store"
4
+ import { discoverProjects, type DiscoveredProject } from "./discovery"
5
+ import { getMachineDisplayName } from "./machine-name"
6
+ import { TerminalManager } from "./terminal-manager"
7
+ import { UpdateManager } from "./update-manager"
8
+ import type { UpdateInstallAttemptResult } from "./cli-runtime"
9
+ import { NatsDaemonManager, type NatsDaemonReadiness } from "./nats-daemon-manager"
10
+ import { NatsConnector } from "./nats-connector"
11
+ import { generateAuthToken } from "./nats-auth"
12
+ import { requiresCalloutForBind } from "./nats-bind-guard"
13
+ import { readToken } from "../nats/nats-token"
14
+ import { ensureCalloutKeys } from "../nats/auth-callout/keys"
15
+ import { mintCredentialToken } from "../nats/auth-callout/token"
16
+ import { createNatsPublisher } from "./nats-publisher"
17
+ import { registerCommandResponders } from "./nats-responders"
18
+ import { registerPtyResponders } from "./pty-responders"
19
+ import { OAuthSettingsStore } from "./oauth-pool/oauth-settings-store"
20
+ import { OAuthTokenPool } from "./oauth-pool/oauth-token-pool"
21
+ import { registerOAuthResponders } from "./oauth-pool/oauth-responders"
22
+ import { ensureTerminalEventsStream, ensureChatMessageStream, ensureRunnerEventsStream, ensureWorkspaceCoordinationStream, ensureSandboxEventsStream, ensureRunnerRegistryBucket } from "./nats-streams"
23
+ import { RunnerManager, type RunnerReadiness } from "./runner-manager"
24
+ import { PairingStore } from "./pairing-store"
25
+ import { resolveRunnerPairingUrls } from "./runner-pairing-urls"
26
+ import { forwardClientLogs } from "./client-log-forwarder"
27
+ import { RunnerProxy } from "./runner-proxy"
28
+ import { RunnerRouter } from "./runner-router"
29
+ import { TranscriptConsumer } from "./transcript-consumer"
30
+ import type { AgentProvider, TranscriptEntry, SessionStatus } from "../shared/types"
31
+ import type { ClientCommand } from "../shared/protocol"
32
+ import { SessionOrchestrator } from "./orchestration"
33
+ import { SessionIndex } from "./session-index"
34
+ import { TranscriptSearchIndex } from "./transcript-search"
35
+ import { WorkspaceAgent } from "./workspace-agent"
36
+ import { createWorkspaceAgentRouter } from "./workspace-agent-routes"
37
+ import { SkillCache } from "./skill-discovery"
38
+ import { WorkspaceConfigManager } from "./workspace-config-manager"
39
+ import { WorkspaceDirectoryPolicy } from "./workspace-directory-policy"
40
+ import { RepoManager } from "./repo-manager"
41
+ import { GitClonePolicy } from "./git-clone-policy"
42
+ import { WorkflowStore } from "./workflow-store"
43
+ import { WorkflowEngine } from "./workflow-engine"
44
+ import { initVapid, PushSubscriptionStore, createPushRouter, sendPushToAll } from "./push-notifications"
45
+ import { BunDockerClient, SandboxManager } from "./sandbox-manager"
46
+ import { RuntimeRegistry } from "./runtime-registry"
47
+ import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk"
48
+ import type { DiscoveredModel } from "../shared/runtime-types"
49
+ import { createExtensionRouter } from "./extension-router"
50
+ import { serverExtensions } from "./extensions.config"
51
+ import { DelegationCoordinator, type DelegationStore } from "./delegation-coordinator"
52
+
53
+ export interface StartServerOptions {
54
+ port?: number
55
+ host?: string
56
+ strictPort?: boolean
57
+ sandbox?: boolean
58
+ onMigrationProgress?: (message: string) => void
59
+ update?: {
60
+ version: string
61
+ fetchLatestVersion: (packageName: string) => Promise<string>
62
+ installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
63
+ }
64
+ }
65
+
66
+ export const NATS_WS_PROXY_BUFFER_LIMIT = 256
67
+
68
+ export type NatsWsProxyCounters = {
69
+ upgrades: number
70
+ upstreamOpen: number
71
+ upstreamError: number
72
+ upstreamClose: number
73
+ sendOnConnecting: number
74
+ bufferedFrames: number
75
+ bufferDrops: number
76
+ // Lazy-mode visibility: number of clients we replayed cached INFO to,
77
+ // and number of duplicate INFO frames we suppressed from the real upstream.
78
+ cacheReplay: number
79
+ firstUpstreamDropped: number
80
+ lazyHelloDeferred: number
81
+ }
82
+
83
+ // Strings or owned ArrayBuffers — never Bun's recv Buffer slices, which get
84
+ // reused out from under us.
85
+ type NatsWsBufferedFrame = string | ArrayBuffer
86
+
87
+ export type NatsWsProxyData = {
88
+ wsPort: number
89
+ upstream: WebSocket | null
90
+ ready: boolean
91
+ closed: boolean
92
+ buffer: NatsWsBufferedFrame[]
93
+ droppedSinceLastLog: number
94
+ openedAt: number
95
+ // Lazy mode: true between proxy.open and the first client frame.
96
+ // While true, we have NOT opened the upstream yet — the browser sees a
97
+ // synthetic INFO replayed from cache and is waiting to send its CONNECT.
98
+ awaitingHello: boolean
99
+ // Lazy mode: drop the first frame the upstream sends us once it opens.
100
+ // That frame is NATS's own INFO, which would be a duplicate of what the
101
+ // browser already received from cache.
102
+ skipFirstUpstreamFrame: boolean
103
+ }
104
+
105
+ type IncomingWsMessage = string | Buffer<ArrayBuffer>
106
+
107
+ function toBufferedFrame(message: IncomingWsMessage): NatsWsBufferedFrame {
108
+ if (typeof message === "string") return message
109
+ const copy = new ArrayBuffer(message.byteLength)
110
+ new Uint8Array(copy).set(message)
111
+ return copy
112
+ }
113
+
114
+ export interface CachedNatsInfo {
115
+ bytes: Uint8Array
116
+ isBinary: boolean
117
+ }
118
+
119
+ // Warmup helper: opens a one-shot WebSocket to NATS and captures the very
120
+ // first frame (the INFO line) raw. We need this so the proxy can replay INFO
121
+ // to browser clients WITHOUT having to open an upstream connection first —
122
+ // which is what causes NATS's 2-second auth-timeout clock to start before
123
+ // the browser has had time to send CONNECT over a high-latency path.
124
+ export async function warmupCachedNatsInfo(
125
+ natsWsUrl: string,
126
+ timeoutMs: number = 5000,
127
+ ): Promise<CachedNatsInfo | null> {
128
+ return await new Promise<CachedNatsInfo | null>((resolve) => {
129
+ let settled = false
130
+ let ws: WebSocket
131
+ const finish = (result: CachedNatsInfo | null) => {
132
+ if (settled) return
133
+ settled = true
134
+ clearTimeout(timer)
135
+ try { ws?.close() } catch (_err: unknown) { /* ignore close errors */ }
136
+ resolve(result)
137
+ }
138
+ const timer = setTimeout(() => {
139
+ console.warn(LOG_PREFIX, `nats-ws INFO warmup timed out after ${timeoutMs}ms`)
140
+ finish(null)
141
+ }, timeoutMs)
142
+ try {
143
+ ws = new WebSocket(natsWsUrl)
144
+ } catch (err) {
145
+ console.warn(LOG_PREFIX, `nats-ws INFO warmup failed to open: ${err instanceof Error ? err.message : String(err)}`)
146
+ finish(null)
147
+ return
148
+ }
149
+ ws.binaryType = "arraybuffer"
150
+ ws.onmessage = (event) => {
151
+ if (settled) return
152
+ if (typeof event.data === "string") {
153
+ finish({ bytes: new TextEncoder().encode(event.data), isBinary: false })
154
+ } else {
155
+ const ab = event.data as ArrayBuffer
156
+ const bytes = new Uint8Array(ab.byteLength)
157
+ bytes.set(new Uint8Array(ab))
158
+ finish({ bytes, isBinary: true })
159
+ }
160
+ }
161
+ ws.onerror = () => {
162
+ console.warn(LOG_PREFIX, "nats-ws INFO warmup WebSocket error")
163
+ finish(null)
164
+ }
165
+ ws.onclose = () => finish(null)
166
+ })
167
+ }
168
+
169
+ export function createNatsWsProxyHandlers(
170
+ counters: NatsWsProxyCounters = {
171
+ upgrades: 0,
172
+ upstreamOpen: 0,
173
+ upstreamError: 0,
174
+ upstreamClose: 0,
175
+ sendOnConnecting: 0,
176
+ bufferedFrames: 0,
177
+ bufferDrops: 0,
178
+ cacheReplay: 0,
179
+ firstUpstreamDropped: 0,
180
+ lazyHelloDeferred: 0,
181
+ },
182
+ cachedInfo: CachedNatsInfo | null = null,
183
+ ) {
184
+ const lazyMode = cachedInfo !== null
185
+
186
+ function openUpstream(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
187
+ const upstream = new WebSocket(`ws://127.0.0.1:${ws.data.wsPort}`)
188
+ upstream.binaryType = "arraybuffer"
189
+ ws.data.upstream = upstream
190
+
191
+ upstream.onopen = () => {
192
+ if (ws.data.closed) {
193
+ upstream.close()
194
+ return
195
+ }
196
+ const elapsed = Date.now() - ws.data.openedAt
197
+ ws.data.ready = true
198
+ counters.upstreamOpen += 1
199
+ console.warn(LOG_PREFIX, `nats-ws proxy upstream open t=${elapsed}ms`)
200
+
201
+ const pending = ws.data.buffer
202
+ ws.data.buffer = []
203
+ for (const frame of pending) {
204
+ upstream.send(frame)
205
+ }
206
+ }
207
+
208
+ upstream.onmessage = (event) => {
209
+ if (ws.readyState !== WebSocket.OPEN) return
210
+ // Lazy mode: drop the very first frame from the upstream — it's NATS's
211
+ // own INFO, which the browser already received from the cache replay at
212
+ // proxy.open. Forwarding it would give the browser a second INFO with a
213
+ // different client_id mid-handshake, which nats-core would treat as a
214
+ // cluster update and could cause it to second-guess the active inbox sub.
215
+ if (ws.data.skipFirstUpstreamFrame) {
216
+ ws.data.skipFirstUpstreamFrame = false
217
+ counters.firstUpstreamDropped += 1
218
+ return
219
+ }
220
+ if (typeof event.data === "string") {
221
+ ws.sendText(event.data)
222
+ } else {
223
+ ws.sendBinary(new Uint8Array(event.data as ArrayBuffer))
224
+ }
225
+ }
226
+
227
+ upstream.onclose = (event) => {
228
+ counters.upstreamClose += 1
229
+ const duration = Date.now() - ws.data.openedAt
230
+ console.warn(
231
+ LOG_PREFIX,
232
+ `nats-ws proxy upstream close code=${event.code} duration=${duration}ms`,
233
+ )
234
+ if (ws.readyState === WebSocket.OPEN) ws.close()
235
+ }
236
+
237
+ upstream.onerror = () => {
238
+ counters.upstreamError += 1
239
+ console.warn(LOG_PREFIX, "nats-ws proxy upstream error")
240
+ if (ws.readyState === WebSocket.OPEN) ws.close()
241
+ }
242
+ }
243
+
244
+ const handlers = {
245
+ open(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
246
+ counters.upgrades += 1
247
+ ws.data.openedAt = Date.now()
248
+ ws.data.ready = false
249
+ ws.data.closed = false
250
+ ws.data.buffer = []
251
+ ws.data.droppedSinceLastLog = 0
252
+ ws.data.awaitingHello = false
253
+ ws.data.skipFirstUpstreamFrame = false
254
+
255
+ if (lazyMode && cachedInfo) {
256
+ // Lazy path: replay cached INFO so the browser's nats-core advances
257
+ // its state machine and prepares to send CONNECT. Defer opening the
258
+ // upstream until the browser actually sends its first frame, so
259
+ // NATS's auth-timeout clock starts only when CONNECT is in our hand.
260
+ console.warn(LOG_PREFIX, "nats-ws proxy upgrade accepted (lazy)")
261
+ ws.data.awaitingHello = true
262
+ ws.data.skipFirstUpstreamFrame = true
263
+ counters.lazyHelloDeferred += 1
264
+ try {
265
+ if (cachedInfo.isBinary) {
266
+ ws.sendBinary(cachedInfo.bytes)
267
+ } else {
268
+ ws.sendText(new TextDecoder().decode(cachedInfo.bytes))
269
+ }
270
+ counters.cacheReplay += 1
271
+ } catch (err) {
272
+ console.warn(
273
+ LOG_PREFIX,
274
+ `nats-ws proxy failed to replay cached INFO: ${err instanceof Error ? err.message : String(err)}`,
275
+ )
276
+ ws.close()
277
+ }
278
+ return
279
+ }
280
+
281
+ // Eager fallback: original behavior used when warmup didn't produce a
282
+ // cache. Logs loudly so the operator knows the auth-race protection is
283
+ // disabled for this run.
284
+ console.warn(LOG_PREFIX, "nats-ws proxy upgrade accepted (eager — no INFO cache)")
285
+ openUpstream(ws)
286
+ },
287
+
288
+ message(ws: import("bun").ServerWebSocket<NatsWsProxyData>, message: IncomingWsMessage) {
289
+ if (ws.data.awaitingHello) {
290
+ // First client frame in lazy mode. Open the upstream NOW and put this
291
+ // frame at the head of the buffer — it'll be the first thing flushed
292
+ // when upstream's onopen fires, so NATS receives CONNECT within one
293
+ // localhost roundtrip of starting its auth clock.
294
+ ws.data.awaitingHello = false
295
+ counters.bufferedFrames += 1
296
+ ws.data.buffer.push(toBufferedFrame(message))
297
+ openUpstream(ws)
298
+ return
299
+ }
300
+
301
+ const upstream = ws.data.upstream
302
+ if (ws.data.ready && upstream && upstream.readyState === WebSocket.OPEN) {
303
+ upstream.send(message)
304
+ ws.data.droppedSinceLastLog = 0
305
+ return
306
+ }
307
+
308
+ counters.sendOnConnecting += 1
309
+ counters.bufferedFrames += 1
310
+ ws.data.buffer.push(toBufferedFrame(message))
311
+
312
+ if (ws.data.buffer.length > NATS_WS_PROXY_BUFFER_LIMIT) {
313
+ ws.data.buffer.shift()
314
+ counters.bufferDrops += 1
315
+ ws.data.droppedSinceLastLog += 1
316
+ if (ws.data.droppedSinceLastLog === 1) {
317
+ console.warn(
318
+ LOG_PREFIX,
319
+ `nats-ws proxy buffer overflow — dropping oldest frame (limit=${NATS_WS_PROXY_BUFFER_LIMIT})`,
320
+ )
321
+ }
322
+ }
323
+ },
324
+ close(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
325
+ ws.data.closed = true
326
+ const duration = Date.now() - ws.data.openedAt
327
+ console.warn(LOG_PREFIX, `nats-ws proxy client close duration=${duration}ms`)
328
+ ws.data.buffer = []
329
+ ws.data.upstream?.close()
330
+ ws.data.upstream = null
331
+ ws.data.ready = false
332
+ ws.data.awaitingHello = false
333
+ },
334
+ }
335
+ return { handlers, counters }
336
+ }
337
+
338
+ export interface ServerHealthcheck {
339
+ ok: boolean
340
+ status: "ok" | "degraded" | "fail"
341
+ port: number
342
+ natsWsPort: number
343
+ natsDaemon: NatsDaemonReadiness | null
344
+ natsConnection: ReturnType<NatsConnector["getReadiness"]>
345
+ runner: RunnerReadiness
346
+ /** PR5: all registered runners from the KV fleet. Only present in /health responses. Empty array when NATS is not ready. */
347
+ runners?: import("./runner-router").RunnerDescriptor[]
348
+ }
349
+
350
+ /** Session coordinator interface — satisfied by RunnerProxy which delegates turn execution to the runner process. */
351
+ interface SessionCoordinator {
352
+ send(command: Extract<ClientCommand, { type: "chat.send" }>): Promise<{ chatId: string }>
353
+ queue(command: Extract<ClientCommand, { type: "chat.queue" }>): Promise<{ chatId: string; queued: boolean }>
354
+ drainQueuedTurn(chatId: string): Promise<boolean>
355
+ cancel(chatId: string): Promise<void>
356
+ respondTool(command: Extract<ClientCommand, { type: "chat.respondTool" }>): Promise<void>
357
+ disposeChat(chatId: string): Promise<void>
358
+ getActiveStatuses(): Map<string, SessionStatus>
359
+ activeTurns: { has(key: string): boolean }
360
+ startTurnForChat(args: {
361
+ chatId: string; provider: AgentProvider; content: string
362
+ delegatedContext?: string; isSpawned?: boolean; model: string; effort?: string
363
+ serviceTier?: "fast"; planMode: boolean; appendUserPrompt: boolean
364
+ }): Promise<void>
365
+ }
366
+
367
+ export async function startServer(options: StartServerOptions = {}) {
368
+ const port = options.port ?? 3210
369
+ const hostname = options.host ?? "127.0.0.1"
370
+ const strictPort = options.strictPort ?? false
371
+ const store = new EventStore()
372
+ const machineDisplayName = getMachineDisplayName()
373
+ await store.initialize()
374
+ await store.migrateLegacyTranscripts(options.onMigrationProgress)
375
+ let discoveredProjects: DiscoveredProject[] = []
376
+
377
+ async function refreshDiscovery() {
378
+ discoveredProjects = discoverProjects()
379
+ return discoveredProjects
380
+ }
381
+
382
+ await refreshDiscovery()
383
+
384
+ let server: ReturnType<typeof Bun.serve>
385
+ const terminals = new TerminalManager()
386
+ const updateManager = options.update
387
+ ? new UpdateManager({
388
+ currentVersion: options.update.version,
389
+ fetchLatestVersion: options.update.fetchLatestVersion,
390
+ installVersion: options.update.installVersion,
391
+ devMode: getRuntimeProfile() === "dev",
392
+ })
393
+ : null
394
+
395
+ const natsMode = process.env.NATS_MODE ?? "embedded"
396
+ const authMode = process.env.NATS_AUTH_MODE ?? "callout"
397
+ const runnerMode = process.env.RUNNER_MODE ?? "spawn"
398
+
399
+ // Guard: a non-loopback bind is only safe in callout mode (decision 0007).
400
+ // Token mode must stay loopback-only — a shared-token bus must not be exposed
401
+ // beyond the local machine. Callout mode provides per-connection scoped creds,
402
+ // so WireGuard on the tailnet is sufficient for confidentiality (no TLS needed).
403
+ const bindGuard = requiresCalloutForBind(hostname, authMode as "callout" | "token")
404
+ if (!bindGuard.ok) {
405
+ throw new Error(bindGuard.reason!)
406
+ }
407
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1" && authMode === "callout") {
408
+ console.warn(LOG_PREFIX, `Binding NATS to ${hostname} in callout mode — confidentiality via WireGuard, WS no_tls within the tailnet`)
409
+ }
410
+
411
+ let authToken: string
412
+ let daemonManager: NatsDaemonManager
413
+ let daemonInfo: { url: string; wsUrl: string; wsPort: number }
414
+
415
+ // Callout mode: stateless signed tokens per connection class.
416
+ // Token mode (NATS_AUTH_MODE=token): shared static token — legacy escape hatch.
417
+ let uiClientToken: string // returned by /auth/token for browser WS connections
418
+ let mintRunnerToken: ((runnerId: string) => Promise<string>) | undefined
419
+ // Durable runner credential mint for pairing (callout mode only).
420
+ // RUNNER_PAIR_TTL: long-lived token TTL in seconds (default 90 days).
421
+ // Paired runners persist this token and present it at every NATS connect.
422
+ const runnerPairTtl = Number(process.env.RUNNER_PAIR_TTL ?? 7_776_000)
423
+ let mintPairedRunnerToken: ((runnerId: string) => Promise<string>) | undefined
424
+
425
+ if (natsMode === "external") {
426
+ const natsUrl = process.env.NATS_URL
427
+ const natsWsPort = Number(process.env.NATS_WS_PORT)
428
+ const natsDataDir = process.env.NATS_DATA_DIR
429
+ if (!natsUrl || !natsWsPort || !natsDataDir) {
430
+ throw new Error("NATS_MODE=external requires NATS_URL, NATS_WS_PORT, and NATS_DATA_DIR")
431
+ }
432
+ authToken = await readToken(natsDataDir)
433
+ uiClientToken = authToken
434
+ daemonManager = NatsDaemonManager.fromExternal({ natsUrl, wsPort: natsWsPort })
435
+ const url = new URL(natsUrl)
436
+ daemonInfo = { url: natsUrl, wsUrl: `ws://${url.hostname}:${natsWsPort}`, wsPort: natsWsPort }
437
+ console.warn(LOG_PREFIX, `NATS_MODE=external — connecting to ${natsUrl}`)
438
+ } else if (authMode === "callout") {
439
+ // Callout mode: load (or generate) signing keys + shared token secret.
440
+ const natsDataDir = process.env.NATS_DATA_DIR ?? store.dataDir
441
+ // The callout daemon child (spawned by ensureDaemon) loads its keys + token
442
+ // secret from NATS_DATA_DIR and refuses to start without it. ensureDaemon
443
+ // reads it from the environment, so default the env to the resolved dir here
444
+ // — otherwise a normal `tinkaria` run (no NATS_DATA_DIR set) fails to boot.
445
+ process.env.NATS_DATA_DIR ??= natsDataDir
446
+ const keys = await ensureCalloutKeys(natsDataDir)
447
+
448
+ authToken = await mintCredentialToken({ class: "server-admin" }, keys.tokenSecret)
449
+ uiClientToken = await mintCredentialToken({ class: "ui-client" }, keys.tokenSecret)
450
+ mintRunnerToken = (runnerId: string) =>
451
+ mintCredentialToken({ class: "runner", runnerId }, keys.tokenSecret)
452
+ // Paired runners get a long-lived token (RUNNER_PAIR_TTL, default 90d).
453
+ mintPairedRunnerToken = (runnerId: string) =>
454
+ mintCredentialToken({ class: "runner", runnerId }, keys.tokenSecret, runnerPairTtl)
455
+
456
+ daemonManager = NatsDaemonManager.embedded()
457
+ const info = await daemonManager.ensureDaemon({ token: authToken, host: hostname })
458
+ daemonInfo = info
459
+ console.warn(LOG_PREFIX, `NATS_AUTH_MODE=callout — scoped credentials active`)
460
+ } else {
461
+ // Token mode (legacy escape hatch): shared static token, token-mode daemon.
462
+ authToken = generateAuthToken()
463
+ uiClientToken = authToken
464
+ daemonManager = NatsDaemonManager.embedded()
465
+ const info = await daemonManager.ensureDaemon({ token: authToken, host: hostname })
466
+ daemonInfo = info
467
+ }
468
+
469
+ // Pairing code store (callout mode only — see POST /api/pairing/code).
470
+ const pairingStore = new PairingStore()
471
+
472
+ // Push notifications
473
+ initVapid()
474
+ const pushStore = new PushSubscriptionStore(path.join(store.dataDir, "push-subscriptions.json"))
475
+ await pushStore.load()
476
+ const pushRouter = createPushRouter(pushStore)
477
+
478
+ const natsConnector = await NatsConnector.connect({
479
+ natsUrl: daemonInfo.url,
480
+ natsWsUrl: daemonInfo.wsUrl,
481
+ natsWsPort: daemonInfo.wsPort,
482
+ token: authToken,
483
+ })
484
+ await Promise.all([
485
+ ensureTerminalEventsStream(natsConnector.nc),
486
+ ensureChatMessageStream(natsConnector.nc),
487
+ ensureRunnerEventsStream(natsConnector.nc),
488
+ ensureWorkspaceCoordinationStream(natsConnector.nc),
489
+ ensureSandboxEventsStream(natsConnector.nc),
490
+ // Pre-create the runner registry KV bucket so spawned runners (whose
491
+ // callout scope excludes STREAM.CREATE) can open it immediately.
492
+ ensureRunnerRegistryBucket(natsConnector.nc),
493
+ ])
494
+
495
+ const getHealthcheck = (): ServerHealthcheck => {
496
+ const natsDaemon = daemonManager.getReadiness()
497
+ const natsConnection = natsConnector.getReadiness()
498
+ const runner = runnerManager.getReadiness()
499
+ const hardFailure = !natsDaemon?.ok || !natsConnection.ok || !runner.ok
500
+ return {
501
+ ok: !hardFailure,
502
+ status: hardFailure ? "fail" : "ok",
503
+ port: actualPort,
504
+ natsWsPort: natsConnector.natsWsPort,
505
+ natsDaemon,
506
+ natsConnection,
507
+ runner,
508
+ }
509
+ }
510
+
511
+ // Project agent: cross-session awareness and coordination
512
+ const sessionIndex = new SessionIndex()
513
+ const transcriptSearch = new TranscriptSearchIndex()
514
+ const projectAgent = new WorkspaceAgent({
515
+ sessions: sessionIndex,
516
+ store,
517
+ search: transcriptSearch,
518
+ workspaceId: "",
519
+ })
520
+ const projectAgentRouter = createWorkspaceAgentRouter(projectAgent)
521
+ const extensionRouter = createExtensionRouter(serverExtensions)
522
+
523
+ // Use indirection to break the circular dependency:
524
+ // coordinator -> onStateChange -> publisher.broadcastSnapshots
525
+ // publisher -> coordinator.getActiveStatuses
526
+ //
527
+ // Debounce: during streaming, dozens of events arrive per second.
528
+ // Each calls onStateChange() which would trigger broadcastSnapshots().
529
+ // Coalesce into one broadcast per microtask tick using queueMicrotask.
530
+ // Selective invalidation: only recompute topic types that changed.
531
+ let broadcastPending = false
532
+ const pendingTypes = new Set<string>()
533
+ let broadcastFn: (changedTypes?: ReadonlySet<string>) => void = () => {}
534
+ const broadcast = (changedTypes?: ReadonlySet<string>) => {
535
+ if (changedTypes) {
536
+ for (const t of changedTypes) pendingTypes.add(t)
537
+ }
538
+ if (broadcastPending) return
539
+ broadcastPending = true
540
+ queueMicrotask(() => {
541
+ broadcastPending = false
542
+ const types = pendingTypes.size > 0 ? new Set(pendingTypes) : undefined
543
+ pendingTypes.clear()
544
+ broadcastFn(types)
545
+ })
546
+ }
547
+ let publishMessage: (chatId: string, entry: TranscriptEntry) => void = () => {}
548
+ let reconcileDelegation: (workspaceId: string, chatId: string, outcome: "success" | "failed" | "cancelled") => void = () => {}
549
+
550
+ const onMessageAppended = (chatId: string, entry: TranscriptEntry) => {
551
+ publishMessage(chatId, entry)
552
+ sessionIndex.onMessageAppended(chatId, entry, store.state)
553
+ transcriptSearch.addEntry(chatId, entry)
554
+ orchestrator.onMessageAppended(chatId, entry)
555
+ }
556
+
557
+ const skillCache = new SkillCache()
558
+
559
+ // ── Runner process handles all turn execution ──
560
+ const runnerManager = new RunnerManager({
561
+ nc: natsConnector.nc,
562
+ natsUrl: daemonInfo.url,
563
+ authToken,
564
+ mintToken: mintRunnerToken,
565
+ mode: runnerMode as "spawn" | "discover",
566
+ })
567
+ const runnerId = await runnerManager.ensureRunner()
568
+
569
+ const chatSidebarTypes = new Set(["chat", "sidebar", "orchestration"])
570
+ const previousStatuses = new Map<string, string>()
571
+ const transcriptConsumer = new TranscriptConsumer({
572
+ nc: natsConnector.nc,
573
+ store,
574
+ onStateChange: () => {
575
+ broadcast(chatSidebarTypes)
576
+ // Push notification triggers
577
+ const currentActive = transcriptConsumer.getActiveStatuses()
578
+ // Check for newly waiting_for_user
579
+ for (const [chatId, status] of currentActive) {
580
+ const prev = previousStatuses.get(chatId)
581
+ if (status === "waiting_for_user" && prev !== "waiting_for_user") {
582
+ const chat = store.state.chatsById.get(chatId)
583
+ void sendPushToAll(pushStore, {
584
+ title: "Input needed",
585
+ body: chat?.title || chatId,
586
+ url: `/chat/${chatId}`,
587
+ tag: `waiting-${chatId}`,
588
+ })
589
+ }
590
+ }
591
+ // Check for chats that were active but are now gone (turn ended)
592
+ for (const [chatId, prevStatus] of previousStatuses) {
593
+ if (!currentActive.has(chatId) && prevStatus !== "waiting_for_user") {
594
+ const chat = store.state.chatsById.get(chatId)
595
+ const outcome = chat?.lastTurnOutcome
596
+ if (outcome === "success") {
597
+ void sendPushToAll(pushStore, {
598
+ title: "Agent finished",
599
+ body: chat?.title || chatId,
600
+ url: `/chat/${chatId}`,
601
+ tag: `turn-${chatId}`,
602
+ })
603
+ } else if (outcome === "failed") {
604
+ void sendPushToAll(pushStore, {
605
+ title: "Agent failed",
606
+ body: chat?.title || chatId,
607
+ url: `/chat/${chatId}`,
608
+ tag: `turn-${chatId}`,
609
+ })
610
+ }
611
+
612
+ // Delegation reconciliation: if this chat was a delegation child, reconcile and possibly resume the parent
613
+ const chatRecord = store.state.chatsById.get(chatId)
614
+ if (chatRecord?.lastTurnOutcome) {
615
+ reconcileDelegation(chatRecord.workspaceId, chatId, chatRecord.lastTurnOutcome)
616
+ }
617
+
618
+ void coordinator.drainQueuedTurn(chatId).catch((error) => {
619
+ console.warn(LOG_PREFIX, "queued chat drain failed:", error instanceof Error ? error.message : String(error))
620
+ })
621
+ }
622
+ }
623
+ // Sync previousStatuses
624
+ previousStatuses.clear()
625
+ for (const [chatId, status] of currentActive) {
626
+ previousStatuses.set(chatId, status)
627
+ }
628
+ },
629
+ onMessageAppended,
630
+ })
631
+
632
+ const runtimeRegistry = new RuntimeRegistry(path.join(store.dataDir, "runtimes"), {
633
+ probeClaudeModels: async (binaryPath: string): Promise<DiscoveredModel[]> => {
634
+ const q = sdkQuery({
635
+ prompt: "",
636
+ options: {
637
+ pathToClaudeCodeExecutable: binaryPath,
638
+ tools: [],
639
+ permissionMode: "bypassPermissions",
640
+ persistSession: false,
641
+ },
642
+ })
643
+ let timer: ReturnType<typeof setTimeout>
644
+ try {
645
+ const models = await Promise.race([
646
+ q.supportedModels(),
647
+ new Promise<never>((_, reject) => {
648
+ timer = setTimeout(() => reject(new Error("Model probe timed out")), 15_000)
649
+ }),
650
+ ])
651
+ return models.map((m) => ({
652
+ value: m.value,
653
+ displayName: m.displayName,
654
+ description: m.description,
655
+ supportsEffort: m.supportsEffort,
656
+ supportedEffortLevels: m.supportedEffortLevels,
657
+ supportsAdaptiveThinking: m.supportsAdaptiveThinking,
658
+ supportsFastMode: m.supportsFastMode,
659
+ supportsAutoMode: m.supportsAutoMode,
660
+ }))
661
+ } finally {
662
+ clearTimeout(timer!)
663
+ try { q.close() } catch (_) { /* close may fail on timed-out probes */ }
664
+ }
665
+ },
666
+ })
667
+ await runtimeRegistry.initialize()
668
+
669
+ const runnerRouter = new RunnerRouter({
670
+ nc: natsConnector.nc,
671
+ sharedRunnerId: () => {
672
+ try { return runnerManager.getRunnerId() } catch { return null }
673
+ },
674
+ })
675
+
676
+ const coordinator: SessionCoordinator = new RunnerProxy({
677
+ nc: natsConnector.nc,
678
+ store,
679
+ runnerId,
680
+ getActiveStatuses: () => transcriptConsumer.getActiveStatuses(),
681
+ runtimeRegistry,
682
+ router: runnerRouter,
683
+ sharedRunnerId: () => runnerManager.getRunnerId(),
684
+ getRunnerReadiness: (targetRunnerId: string) => {
685
+ // For the shared/local runner, use the authoritative RunnerManager readiness.
686
+ // For personal runners, we keep the gate synchronous by failing closed when
687
+ // the descriptor has not been pre-fetched. resolveRunnerForChat already
688
+ // called router.select (which calls router.list), but that descriptor is not
689
+ // cached here. To avoid making getRunnerReadiness async (which would ripple
690
+ // into sendCommand and every call-site), we apply a conservative policy:
691
+ // the shared runner uses its live readiness; a selected personal runner
692
+ // passes the gate (fail-open for the readiness check — the eligibleFor()
693
+ // filter in selectFrom already enforced liveness + compat before selection,
694
+ // so a selected runner is already known-compatible at the time of dispatch).
695
+ if (targetRunnerId === runnerId) {
696
+ return runnerManager.getReadiness()
697
+ }
698
+ // Personal runner: trust that router.select already enforced
699
+ // liveness + protocol compat via eligibleFor(). Return compatible.
700
+ return { incompatible: false, protocolVersion: null, capabilities: null }
701
+ },
702
+ })
703
+
704
+ console.warn(LOG_PREFIX, "Runner process handles turn execution")
705
+
706
+ // ── Durable delegation coordinator ──
707
+ const delegationStoreAdapter: DelegationStore = {
708
+ appendMessage: (chatId, entry) => store.appendMessage(chatId, entry),
709
+ chatExists: (chatId) => {
710
+ const chat = store.state.chatsById.get(chatId)
711
+ return chat != null && chat.deletedAt == null
712
+ },
713
+ getChatWorkspaceId: (chatId) => store.state.chatsById.get(chatId)?.workspaceId,
714
+ getLastTurnOutcome: (chatId) => store.state.chatsById.get(chatId)?.lastTurnOutcome ?? undefined,
715
+ }
716
+ const delegationCoordinator = new DelegationCoordinator(natsConnector.nc, delegationStoreAdapter)
717
+ await delegationCoordinator.initialize()
718
+ await delegationCoordinator.bootReconciliation()
719
+
720
+ const orchestrator = new SessionOrchestrator({
721
+ store,
722
+ coordinator,
723
+ delegationCoordinator,
724
+ })
725
+
726
+ // Start after orchestrator exists — onMessageAppended closes over it
727
+ await transcriptConsumer.start()
728
+
729
+ const publisher = await createNatsPublisher({
730
+ nc: natsConnector.nc,
731
+ store,
732
+ agent: coordinator,
733
+ terminals,
734
+ refreshDiscovery,
735
+ getDiscoveredProjects: () => discoveredProjects,
736
+ machineDisplayName,
737
+ updateManager,
738
+ skillCache,
739
+ orchestrator,
740
+ runtimeRegistry,
741
+ hasActiveBlockingDelegations: delegationCoordinator.hasActiveBlockingDelegations.bind(delegationCoordinator),
742
+ })
743
+
744
+ broadcastFn = (types) => publisher.broadcastSnapshots(types)
745
+ publishMessage = (chatId, entry) => publisher.publishChatMessage(chatId, entry)
746
+ reconcileDelegation = (workspaceId, chatId, outcome) => {
747
+ void delegationCoordinator
748
+ .reconcileChildTerminal(workspaceId, chatId, { outcome })
749
+ .then((result) => {
750
+ if (result && !("alreadyReconciled" in result) && result.resumeEligible) {
751
+ void coordinator.drainQueuedTurn(result.parentChatId).catch((err) => {
752
+ console.warn(LOG_PREFIX, "delegation parent drain failed:", err instanceof Error ? err.message : String(err))
753
+ })
754
+ }
755
+ })
756
+ .catch((err) => {
757
+ console.warn(LOG_PREFIX, "delegation reconciliation failed:", err instanceof Error ? err.message : String(err))
758
+ })
759
+ }
760
+
761
+ const responders = registerCommandResponders({
762
+ nc: natsConnector.nc,
763
+ store,
764
+ agent: coordinator,
765
+ terminals,
766
+ refreshDiscovery,
767
+ updateManager,
768
+ publisher,
769
+ onStateChange: () => publisher.broadcastSnapshots(),
770
+ repoManager: new RepoManager(),
771
+ clonePolicy: new GitClonePolicy(store, new RepoManager(), () => publisher.broadcastSnapshots()),
772
+ directoryPolicy: new WorkspaceDirectoryPolicy(
773
+ store,
774
+ new WorkspaceConfigManager(path.join(store.dataDir, "workspaces")),
775
+ () => publisher.broadcastSnapshots(),
776
+ ),
777
+ workflowStore: new WorkflowStore(path.join(store.dataDir, "workflows")),
778
+ workflowEngine: new WorkflowEngine({
779
+ emitter: store,
780
+ dispatcher: { dispatch: async () => "" },
781
+ resolveRepos: async (workspaceId: string) => {
782
+ return [...store.state.reposById.values()]
783
+ .filter((r) => r.workspaceId === workspaceId)
784
+ .map((r) => r.id)
785
+ },
786
+ onProgress: () => publisher.broadcastSnapshots(),
787
+ }),
788
+ sandboxManager: options.sandbox ? new SandboxManager(new BunDockerClient(), "nats://host.docker.internal:4222") : null,
789
+ runtimeRegistry,
790
+ })
791
+
792
+ const oauthSettings = new OAuthSettingsStore()
793
+ await oauthSettings.load()
794
+ const oauthPool = new OAuthTokenPool(
795
+ () => oauthSettings.getTokens(),
796
+ (id, patch) => oauthSettings.mutateTokenStatus(id, patch),
797
+ Date.now,
798
+ () => oauthSettings.getConcurrencyDefault(),
799
+ )
800
+ const oauthResponders = registerOAuthResponders({ nc: natsConnector.nc, store: oauthSettings })
801
+
802
+ const ptyResponders = registerPtyResponders({ nc: natsConnector.nc, store, oauthPool })
803
+
804
+ // Boot-time self-test: round-trip pty.snapshot through NATS to prove the
805
+ // responder is reachable. Logs a single line so /health smoke can confirm.
806
+ ;(async () => {
807
+ try {
808
+ const { ptyCommandSubject } = await import("../shared/nats-subjects")
809
+ const { compressPayload, decompressPayload } = await import("../shared/compression")
810
+ const enc = new TextEncoder()
811
+ const dec = new TextDecoder()
812
+ const reply = await natsConnector.nc.request(
813
+ ptyCommandSubject("pty.snapshot"),
814
+ compressPayload(enc.encode("{}")),
815
+ { timeout: 2000 },
816
+ )
817
+ const text = dec.decode(await decompressPayload(reply.data))
818
+ console.log(LOG_PREFIX, "claude-pty self-test pty.snapshot:", text)
819
+
820
+ // Spawn smoke: prove driver invocation reaches verifyPtyAuth by issuing
821
+ // pty.spawn with no OAuth token. Expected reply: {ok:false, error:"…OAuth pool token…"}.
822
+ const spawnReply = await natsConnector.nc.request(
823
+ ptyCommandSubject("pty.spawn"),
824
+ compressPayload(enc.encode(JSON.stringify({
825
+ chatId: "smoke-spawn-no-token",
826
+ projectId: "smoke",
827
+ cwd: process.cwd(),
828
+ model: "opus",
829
+ oauthToken: null,
830
+ oauthLabel: "smoke-test",
831
+ }))),
832
+ { timeout: 2000 },
833
+ )
834
+ const spawnText = dec.decode(await decompressPayload(spawnReply.data))
835
+ console.log(LOG_PREFIX, "claude-pty self-test pty.spawn (no token):", spawnText)
836
+
837
+ // Self-test spawned a real PTY when pool had tokens — kill it so the
838
+ // smoke-spawn-no-token instance does not leak into every chat's PTY
839
+ // indicator (publishes `removed` delta to client store).
840
+ try {
841
+ await natsConnector.nc.request(
842
+ ptyCommandSubject("pty.exit"),
843
+ compressPayload(enc.encode(JSON.stringify({ chatId: "smoke-spawn-no-token" }))),
844
+ { timeout: 2000 },
845
+ )
846
+ } catch {
847
+ // Best-effort cleanup; ignore failures (instance may not exist if
848
+ // spawn rejected due to empty pool).
849
+ }
850
+
851
+ const { oauthCommandSubject } = await import("../shared/nats-subjects")
852
+ const oauthListReply = await natsConnector.nc.request(
853
+ oauthCommandSubject("oauth.list"),
854
+ compressPayload(enc.encode("{}")),
855
+ { timeout: 2000 },
856
+ )
857
+ const oauthListText = dec.decode(await decompressPayload(oauthListReply.data))
858
+ console.log(LOG_PREFIX, "oauth-pool self-test oauth.list:", oauthListText)
859
+ } catch (err) {
860
+ console.warn(LOG_PREFIX, "claude-pty self-test failed:", err instanceof Error ? err.message : String(err))
861
+ }
862
+ })().catch(() => undefined)
863
+
864
+ const distDir = path.join(import.meta.dir, "..", "..", "dist", "client")
865
+
866
+ // Warm up the synthetic INFO cache used by the lazy-upstream proxy path.
867
+ // See createNatsWsProxyHandlers / warmupCachedNatsInfo for why this exists.
868
+ // If warmup fails we fall back to eager mode (the legacy auth-race path).
869
+ const warmupUrl = `ws://127.0.0.1:${natsConnector.natsWsPort}`
870
+ const cachedNatsInfo = await warmupCachedNatsInfo(warmupUrl)
871
+ if (cachedNatsInfo) {
872
+ console.warn(
873
+ LOG_PREFIX,
874
+ `nats-ws INFO cache warmed (${cachedNatsInfo.bytes.length} bytes, ${cachedNatsInfo.isBinary ? "binary" : "text"})`,
875
+ )
876
+ } else {
877
+ console.warn(
878
+ LOG_PREFIX,
879
+ "nats-ws INFO cache UNAVAILABLE — proxy will fall back to eager upstream (auth-race risk on slow networks)",
880
+ )
881
+ }
882
+
883
+ const { handlers: natsWsHandlers, counters: natsWsCounters } = createNatsWsProxyHandlers(undefined, cachedNatsInfo)
884
+ const natsWsCounterInterval = setInterval(() => {
885
+ const hasActivity = Object.values(natsWsCounters).some((v) => v > 0)
886
+ if (!hasActivity) return
887
+ console.warn(LOG_PREFIX, "nats-ws proxy counters:", JSON.stringify(natsWsCounters))
888
+ for (const key of Object.keys(natsWsCounters) as (keyof NatsWsProxyCounters)[]) {
889
+ natsWsCounters[key] = 0
890
+ }
891
+ }, 60_000)
892
+
893
+ const MAX_PORT_ATTEMPTS = 20
894
+ let actualPort = port
895
+
896
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
897
+ try {
898
+ server = Bun.serve<NatsWsProxyData>({
899
+ port: actualPort,
900
+ hostname,
901
+ async fetch(req, srv) {
902
+ const url = new URL(req.url)
903
+
904
+ if (url.pathname === "/nats-ws") {
905
+ const upgraded = srv.upgrade(req, {
906
+ data: {
907
+ wsPort: natsConnector.natsWsPort,
908
+ upstream: null,
909
+ ready: false,
910
+ closed: false,
911
+ buffer: [],
912
+ droppedSinceLastLog: 0,
913
+ openedAt: 0,
914
+ awaitingHello: false,
915
+ skipFirstUpstreamFrame: false,
916
+ },
917
+ })
918
+ if (upgraded) return undefined
919
+ return new Response("WebSocket upgrade failed", { status: 426 })
920
+ }
921
+
922
+ if (url.pathname === "/health") {
923
+ const healthcheck = getHealthcheck()
924
+ // runners is async (KV list); fetch in parallel with the sync healthcheck.
925
+ // Failures return [] so the existing ok/status logic is never blocked.
926
+ const runners = await runnerRouter.list().catch(() => [])
927
+ return Response.json({ ...healthcheck, runners }, {
928
+ status: healthcheck.ok ? 200 : 503,
929
+ })
930
+ }
931
+
932
+ if (url.pathname === "/auth/token") {
933
+ const advertisedHost = process.env.NATS_ADVERTISED_HOST
934
+ const natsWsUrl = advertisedHost
935
+ ? `ws://${advertisedHost}:${natsConnector.natsWsPort}`
936
+ : undefined
937
+ // In callout mode: return the ui-client scoped token (not the server-admin token).
938
+ // In token mode: uiClientToken === authToken — same behaviour as before.
939
+ return Response.json({
940
+ token: uiClientToken,
941
+ ...(natsWsUrl ? { natsWsUrl } : {}),
942
+ })
943
+ }
944
+
945
+ // ── Pairing endpoints (PR2) ─────────────────────────────────────────
946
+ //
947
+ // Both routes are public (no user auth), matching /auth/token's posture.
948
+ // The code itself is the bearer secret — short TTL, single-use, unguessable.
949
+ // Gating on user-auth is the documented pre-multi-tenant follow-up.
950
+ //
951
+ if (url.pathname === "/api/pairing/code" && req.method === "POST") {
952
+ // Only available in callout mode — the minted token is a callout credential.
953
+ if (!mintPairedRunnerToken) {
954
+ return Response.json(
955
+ { error: "pairing requires NATS_AUTH_MODE=callout" },
956
+ { status: 409 }
957
+ )
958
+ }
959
+ // Allocate a runnerId (same format as runner-manager's spawned runners).
960
+ const newRunnerId = `runner-${Date.now()}-${process.pid}`
961
+ const token = await mintPairedRunnerToken(newRunnerId)
962
+ const { code, expiresAt } = pairingStore.issue({ runnerId: newRunnerId, token })
963
+ console.warn(LOG_PREFIX, `Pairing code issued for runner ${newRunnerId}, expires ${new Date(expiresAt).toISOString()}`)
964
+ return Response.json({ code, expiresAt })
965
+ }
966
+
967
+ if (url.pathname === "/api/pairing/exchange" && req.method === "POST") {
968
+ let body: { code?: unknown }
969
+ try {
970
+ body = await req.json() as { code?: unknown }
971
+ } catch {
972
+ return Response.json({ error: "invalid JSON body" }, { status: 400 })
973
+ }
974
+ const code = body?.code
975
+ if (typeof code !== "string" || !code.trim()) {
976
+ return Response.json({ error: "missing or invalid code" }, { status: 400 })
977
+ }
978
+ const result = pairingStore.exchange(code)
979
+ if (!result.ok) {
980
+ const isGone = result.error === "expired" || result.error === "consumed"
981
+ console.warn(LOG_PREFIX, `Pairing exchange rejected: ${result.error}`)
982
+ return Response.json({ error: result.error }, { status: isGone ? 410 : 400 })
983
+ }
984
+ // A paired runner may be on another machine, so the credential's
985
+ // NATS URL host must be routable from there — never the wildcard
986
+ // bind host (0.0.0.0 / ::), which would resolve to the runner's own
987
+ // loopback and yield ECONNREFUSED. Honor NATS_ADVERTISED_HOST (same
988
+ // as /auth/token); refuse to hand out an unroutable URL otherwise.
989
+ const pairingUrls = resolveRunnerPairingUrls(daemonInfo)
990
+ if (!pairingUrls.ok) {
991
+ console.warn(LOG_PREFIX, `Pairing exchange blocked for runner ${result.runnerId}: ${pairingUrls.error}`)
992
+ return Response.json({ error: pairingUrls.error }, { status: 409 })
993
+ }
994
+ console.warn(LOG_PREFIX, `Pairing exchange succeeded for runner ${result.runnerId}`)
995
+ return Response.json({
996
+ runnerId: result.runnerId,
997
+ token: result.token,
998
+ natsUrl: pairingUrls.natsUrl,
999
+ natsWsUrl: pairingUrls.natsWsUrl,
1000
+ })
1001
+ }
1002
+
1003
+ // ── Client log ingest (decision 0012) ──────────────────────────────
1004
+ // Browser ships console logs + errors here; we forward to VictoriaLogs
1005
+ // (localhost-only) so the browser never touches VL directly. Always
1006
+ // 204 — logging must never error for the client, even if VL is down.
1007
+ if (url.pathname === "/api/logs" && req.method === "POST") {
1008
+ let body: unknown
1009
+ try {
1010
+ body = await req.json()
1011
+ } catch {
1012
+ return new Response(null, { status: 204 })
1013
+ }
1014
+ void forwardClientLogs(body)
1015
+ return new Response(null, { status: 204 })
1016
+ }
1017
+
1018
+ if (url.pathname.startsWith("/api/workspace/")) {
1019
+ return projectAgentRouter(req)
1020
+ }
1021
+
1022
+ if (url.pathname.startsWith("/api/ext/")) {
1023
+ return extensionRouter(req)
1024
+ }
1025
+
1026
+ if (url.pathname.startsWith("/api/push/")) {
1027
+ return pushRouter(req)
1028
+ }
1029
+
1030
+ return serveStatic(distDir, url.pathname)
1031
+ },
1032
+ websocket: natsWsHandlers,
1033
+ })
1034
+ break
1035
+ } catch (err: unknown) {
1036
+ const isAddrInUse =
1037
+ err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "EADDRINUSE"
1038
+ if (!isAddrInUse || strictPort || attempt === MAX_PORT_ATTEMPTS - 1) {
1039
+ throw err
1040
+ }
1041
+ console.log(`Port ${actualPort} is in use, trying ${actualPort + 1}...`)
1042
+ actualPort++
1043
+ }
1044
+ }
1045
+
1046
+ console.warn(LOG_PREFIX, `Operational health initialized — status: ${getHealthcheck().status}`)
1047
+
1048
+ const shutdown = async () => {
1049
+ clearInterval(natsWsCounterInterval)
1050
+ orchestrator.destroy()
1051
+ responders.dispose()
1052
+ ptyResponders.dispose()
1053
+ oauthResponders.dispose()
1054
+ publisher.dispose()
1055
+ terminals.closeAll()
1056
+ transcriptConsumer.stop()
1057
+ await runnerManager.dispose()
1058
+ await natsConnector.dispose()
1059
+ await daemonManager.dispose()
1060
+ await store.compact()
1061
+ server.stop(true)
1062
+ }
1063
+
1064
+ return {
1065
+ port: actualPort,
1066
+ store,
1067
+ updateManager,
1068
+ healthcheck: getHealthcheck,
1069
+ stop: shutdown,
1070
+ }
1071
+ }
1072
+
1073
+ async function serveStatic(distDir: string, pathname: string) {
1074
+ const requestedPath = pathname === "/" ? "/index.html" : pathname
1075
+ const filePath = path.join(distDir, requestedPath)
1076
+ const indexPath = path.join(distDir, "index.html")
1077
+
1078
+ const file = Bun.file(filePath)
1079
+ if (await file.exists()) {
1080
+ const isHashedAsset = requestedPath.startsWith("/assets/")
1081
+ const isHtml = requestedPath.endsWith(".html")
1082
+ const cacheControl = isHashedAsset
1083
+ ? "public, max-age=31536000, immutable"
1084
+ : isHtml
1085
+ ? "no-cache"
1086
+ : undefined
1087
+
1088
+ const headers: Record<string, string> = {}
1089
+ if (cacheControl) headers["Cache-Control"] = cacheControl
1090
+
1091
+ return new Response(file, { headers })
1092
+ }
1093
+
1094
+ const indexFile = Bun.file(indexPath)
1095
+ if (await indexFile.exists()) {
1096
+ return new Response(indexFile, {
1097
+ headers: {
1098
+ "Content-Type": "text/html; charset=utf-8",
1099
+ "Cache-Control": "no-cache",
1100
+ },
1101
+ })
1102
+ }
1103
+
1104
+ return new Response(
1105
+ `${APP_NAME} client bundle not found. Run \`bun run build\` inside workbench/ first.`,
1106
+ { status: 503 }
1107
+ )
1108
+ }