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,323 @@
1
+ import { connect, type NatsConnection, type Subscription } from "@nats-io/transport-node"
2
+ import { wsconnect } from "@nats-io/nats-core"
3
+ import { Kvm } from "@nats-io/kv"
4
+ import { spawnSync } from "node:child_process"
5
+ import { LOG_PREFIX } from "../shared/branding"
6
+ import {
7
+ runnerCmdSubject,
8
+ runnerHeartbeatSubject,
9
+ RUNNER_REGISTRY_BUCKET,
10
+ PROTOCOL_VERSION,
11
+ type RunnerCapabilities,
12
+ type StartTurnCommand,
13
+ type CancelTurnCommand,
14
+ type RespondToolCommand,
15
+ type StopChatPtyCommand,
16
+ type RunnerRegistration,
17
+ type RunnerHeartbeat,
18
+ } from "../shared/runner-protocol"
19
+ import type { AgentProvider } from "../shared/types"
20
+ import { resolveClaudeBinary } from "../server/claude-pty/resolve-binary.adapter"
21
+ import type { RunnerAgent } from "./runner-agent"
22
+
23
+ const encoder = new TextEncoder()
24
+ const decoder = new TextDecoder()
25
+
26
+ export const RUNNER_RECONNECT_OPTIONS = {
27
+ maxReconnectAttempts: -1,
28
+ reconnectTimeWait: 750,
29
+ pingInterval: 15_000,
30
+ maxPingOut: 3,
31
+ } as const
32
+
33
+ export interface ConnectRunnerOptions {
34
+ natsUrl: string
35
+ token?: string | undefined
36
+ connectFn?: typeof connect
37
+ }
38
+
39
+ export async function connectRunner(
40
+ options: ConnectRunnerOptions,
41
+ ): Promise<NatsConnection> {
42
+ const { natsUrl, token } = options
43
+ // ws:// or wss:// → connect through the server's /nats-ws proxy (same path the
44
+ // browser uses, over the tunneled HTTP port) via wsconnect. Otherwise use the
45
+ // TCP transport. Bun provides a global WebSocket, so wsconnect works here.
46
+ const isWs = /^wss?:\/\//i.test(natsUrl)
47
+ const connectFn = options.connectFn ?? (isWs ? wsconnect : connect)
48
+ return connectFn({
49
+ servers: natsUrl,
50
+ ...(token ? { token } : {}),
51
+ ...RUNNER_RECONNECT_OPTIONS,
52
+ })
53
+ }
54
+
55
+ export interface ShutdownConnectionOptions {
56
+ drainTimeoutMs?: number
57
+ }
58
+
59
+ export async function shutdownConnection(
60
+ nc: NatsConnection,
61
+ options: ShutdownConnectionOptions = {},
62
+ ): Promise<void> {
63
+ const drainTimeoutMs = options.drainTimeoutMs ?? 3_000
64
+ try {
65
+ await Promise.race([
66
+ nc.drain(),
67
+ new Promise<never>((_, reject) =>
68
+ setTimeout(() => reject(new Error("drain timeout")), drainTimeoutMs),
69
+ ),
70
+ ])
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error)
73
+ console.warn(LOG_PREFIX, `runner drain timeout or failed: ${message} — falling back to close()`)
74
+ await nc.close().catch((closeError) => {
75
+ const closeMessage = closeError instanceof Error ? closeError.message : String(closeError)
76
+ console.warn(LOG_PREFIX, `runner close() also failed: ${closeMessage}`)
77
+ })
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Probe which agent providers are actually installed on this runner.
83
+ * A provider is present if its binary resolves successfully. Errors are caught
84
+ * and treated as "not installed" so a bad probe never crashes registration.
85
+ *
86
+ * Exported for testing only — callers within this module use `probeProviders()`.
87
+ */
88
+ export async function probeProviders(
89
+ _resolveClaudeBinary?: typeof resolveClaudeBinary,
90
+ ): Promise<RunnerCapabilities> {
91
+ const resolver = _resolveClaudeBinary ?? resolveClaudeBinary
92
+ const providers: AgentProvider[] = []
93
+
94
+ // Probe claude (SDK provider)
95
+ try {
96
+ await resolver({ env: process.env, homeDir: process.env.HOME ?? process.env.USERPROFILE ?? "" })
97
+ providers.push("claude")
98
+ // claude-pty is the same claude binary driven through a PTY (the
99
+ // claude-pty driver) rather than the SDK. It has no separate binary, so
100
+ // if claude resolves, claude-pty is available too. Advertise it explicitly
101
+ // — otherwise the server's capability gate refuses every claude-pty turn.
102
+ providers.push("claude-pty")
103
+ } catch {
104
+ // claude binary not found — exclude claude and claude-pty from capabilities
105
+ }
106
+
107
+ // Probe codex via which (mirrors resolveCodexBinary in turn-factories.ts)
108
+ try {
109
+ const which = spawnSync("which", ["codex"], { encoding: "utf-8", timeout: 3000 })
110
+ if (which.status === 0 && which.stdout.trim()) {
111
+ providers.push("codex")
112
+ }
113
+ } catch {
114
+ // which failed — exclude codex
115
+ }
116
+
117
+ console.warn(LOG_PREFIX, `Probed capabilities: providers=[${providers.join(", ")}]`)
118
+ return { providers }
119
+ }
120
+
121
+ export interface RunnerNatsHandlerOptions {
122
+ nc: NatsConnection
123
+ agent: RunnerAgent
124
+ runnerId: string
125
+ heartbeatIntervalMs?: number
126
+ /** Override the capability probe for testing — if provided, skips the real probe. */
127
+ _probeProviders?: () => Promise<RunnerCapabilities>
128
+ }
129
+
130
+ export class RunnerNatsHandler {
131
+ private readonly nc: NatsConnection
132
+ private readonly agent: RunnerAgent
133
+ private readonly runnerId: string
134
+ private readonly heartbeatIntervalMs: number
135
+ private readonly _probeProviders: () => Promise<RunnerCapabilities>
136
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null
137
+ private subscriptions: Subscription[] = []
138
+ // Cache the registration shape so heartbeats can update lastSeenAt without
139
+ // re-constructing the full object each time.
140
+ private registration: RunnerRegistration | null = null
141
+ // Cached KV handle for the registry bucket — reused across heartbeat
142
+ // lastSeenAt writes instead of re-opening (kvm.open) on every beat.
143
+ private registryKv: Awaited<ReturnType<Kvm["open"]>> | null = null
144
+
145
+ constructor(options: RunnerNatsHandlerOptions) {
146
+ this.nc = options.nc
147
+ this.agent = options.agent
148
+ this.runnerId = options.runnerId
149
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 10_000
150
+ this._probeProviders = options._probeProviders ?? probeProviders
151
+ }
152
+
153
+ async start(): Promise<void> {
154
+ // Register in KV
155
+ await this.register()
156
+
157
+ // Subscribe to commands
158
+ this.subscribeCommand("start_turn", async (data) => {
159
+ const cmd = JSON.parse(data) as StartTurnCommand
160
+ await this.agent.startTurn(cmd)
161
+ })
162
+
163
+ this.subscribeCommand("cancel_turn", async (data) => {
164
+ const cmd = JSON.parse(data) as CancelTurnCommand
165
+ await this.agent.cancel(cmd.chatId)
166
+ })
167
+
168
+ this.subscribeCommand("respond_tool", async (data) => {
169
+ const cmd = JSON.parse(data) as RespondToolCommand
170
+ await this.agent.respondTool(cmd.chatId, cmd.toolUseId, cmd.result)
171
+ })
172
+
173
+ this.subscribeCommand("stop_chat_pty", async (data) => {
174
+ const cmd = JSON.parse(data) as StopChatPtyCommand
175
+ this.agent.stopChatPty(cmd.chatId)
176
+ })
177
+
178
+ this.subscribeCommand("shutdown", async () => {
179
+ this.dispose()
180
+ })
181
+
182
+ // Flush to ensure subscriptions are visible to other connections
183
+ await this.nc.flush()
184
+
185
+ // Start heartbeat
186
+ this.heartbeatTimer = setInterval(() => this.publishHeartbeat(), this.heartbeatIntervalMs)
187
+ this.publishHeartbeat()
188
+ }
189
+
190
+ dispose(): void {
191
+ if (this.heartbeatTimer) {
192
+ clearInterval(this.heartbeatTimer)
193
+ this.heartbeatTimer = null
194
+ }
195
+ for (const sub of this.subscriptions) {
196
+ sub.unsubscribe()
197
+ }
198
+ this.subscriptions = []
199
+ }
200
+
201
+ private subscribeCommand(cmd: string, handler: (data: string) => Promise<void>): void {
202
+ const subject = runnerCmdSubject(this.runnerId, cmd)
203
+ const sub = this.nc.subscribe(subject)
204
+ this.subscriptions.push(sub)
205
+ // Observability (decision 0012): confirm each command subscription is actually
206
+ // established, and surface it if the subscription loop ever ends/errors (e.g. a
207
+ // permission denial would otherwise be silent — unlike the oauth sub which logs).
208
+ console.warn(LOG_PREFIX, `subscribed to command subject ${subject}`)
209
+
210
+ void (async () => {
211
+ try {
212
+ for await (const msg of sub) {
213
+ // Observability (decision 0012): log on receipt so we can tell whether a
214
+ // command actually reaches the runner (vs the subscription being silently
215
+ // denied or messages never delivered).
216
+ console.warn(LOG_PREFIX, `received command ${cmd} on ${subject}`)
217
+ try {
218
+ const data = decoder.decode(msg.data)
219
+ await handler(data)
220
+ msg.respond(encoder.encode(JSON.stringify({ ok: true })))
221
+ } catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error)
223
+ console.warn(LOG_PREFIX, `Runner command ${cmd} failed: ${message}`)
224
+ msg.respond(encoder.encode(JSON.stringify({ ok: false, error: message })))
225
+ }
226
+ }
227
+ console.warn(LOG_PREFIX, `command subscription ${subject} ended (iterator closed)`)
228
+ } catch (err) {
229
+ const message = err instanceof Error ? err.message : String(err)
230
+ console.warn(LOG_PREFIX, `command subscription ${subject} ERRORED: ${message}`)
231
+ }
232
+ })()
233
+ }
234
+
235
+ private async register(): Promise<void> {
236
+ // Allow tests to simulate a protocol skew via RUNNER_PROTOCOL_VERSION env.
237
+ const protocolVersion = Number(process.env.RUNNER_PROTOCOL_VERSION ?? PROTOCOL_VERSION)
238
+
239
+ // Probe installed providers — defensive: a probe error excludes that provider, never crashes.
240
+ let capabilities: RunnerCapabilities
241
+ try {
242
+ capabilities = await this._probeProviders()
243
+ } catch (err) {
244
+ const message = err instanceof Error ? err.message : String(err)
245
+ console.warn(LOG_PREFIX, `Capability probe failed (defaulting to empty): ${message}`)
246
+ capabilities = { providers: [] }
247
+ }
248
+
249
+ const registration: RunnerRegistration = {
250
+ runnerId: this.runnerId,
251
+ pid: process.pid,
252
+ startedAt: Date.now(),
253
+ providers: capabilities.providers,
254
+ protocolVersion,
255
+ capabilities,
256
+ lastSeenAt: Date.now(),
257
+ }
258
+ this.registration = registration
259
+ try {
260
+ const kvm = new Kvm(this.nc)
261
+ const kvStore = await kvm.create(RUNNER_REGISTRY_BUCKET, {
262
+ max_bytes: 1024 * 1024,
263
+ })
264
+ this.registryKv = kvStore
265
+ await kvStore.put(this.runnerId, encoder.encode(JSON.stringify(registration)))
266
+ } catch (error) {
267
+ // KV bucket may already exist — try to open instead
268
+ try {
269
+ const kvm = new Kvm(this.nc)
270
+ const kvStore = await kvm.open(RUNNER_REGISTRY_BUCKET)
271
+ this.registryKv = kvStore
272
+ await kvStore.put(this.runnerId, encoder.encode(JSON.stringify(registration)))
273
+ } catch (innerError) {
274
+ const message = innerError instanceof Error ? innerError.message : String(innerError)
275
+ console.warn(LOG_PREFIX, `Runner KV registration failed: ${message}`)
276
+ }
277
+ }
278
+ }
279
+
280
+ private publishHeartbeat(): void {
281
+ const heartbeat: RunnerHeartbeat = {
282
+ runnerId: this.runnerId,
283
+ activeChatIds: [...this.agent.activeTurns.keys()],
284
+ ts: Date.now(),
285
+ }
286
+ try {
287
+ this.nc.publish(
288
+ runnerHeartbeatSubject(this.runnerId),
289
+ encoder.encode(JSON.stringify(heartbeat))
290
+ )
291
+ } catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error)
293
+ console.warn(LOG_PREFIX, `runner heartbeat publish failed: ${message}`)
294
+ }
295
+ // Update lastSeenAt in KV on every heartbeat so the discover path (no live
296
+ // subscription) has a fresh TTL signal. Fire-and-forget; a missed write just
297
+ // means the cached value ages naturally — still bounded by heartbeatIntervalMs.
298
+ this.updateLastSeenAt()
299
+ }
300
+
301
+ private updateLastSeenAt(): void {
302
+ if (!this.registration) return
303
+ const updated: RunnerRegistration = { ...this.registration, lastSeenAt: Date.now() }
304
+ this.registration = updated
305
+ void (async () => {
306
+ try {
307
+ // Reuse the cached KV handle from register(); open once if absent.
308
+ if (!this.registryKv) {
309
+ this.registryKv = await new Kvm(this.nc).open(RUNNER_REGISTRY_BUCKET)
310
+ }
311
+ await this.registryKv.put(this.runnerId, encoder.encode(JSON.stringify(updated)))
312
+ } catch {
313
+ // Best-effort; drop the (possibly stale) handle so the next beat re-opens.
314
+ this.registryKv = null
315
+ }
316
+ })()
317
+ }
318
+
319
+ /** Test-only wrapper for the private heartbeat publisher. */
320
+ publishHeartbeatForTest(): void {
321
+ this.publishHeartbeat()
322
+ }
323
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tests for the runner pair flow (PR2 Stage 2).
3
+ *
4
+ * Spins up a real embedded server on a dedicated port, exercises the
5
+ * POST /api/pairing/exchange path, and checks that writeRunnerCredential
6
+ * is called with the correct shape. Uses a real pairing code issued by
7
+ * the server (integration-style, same pattern as pairing-endpoints.test.ts).
8
+ *
9
+ * Also tests error propagation (expired/consumed → throws, unknown → throws).
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
13
+ import { mkdtempSync, rmSync } from "node:fs"
14
+ import { join } from "node:path"
15
+ import { tmpdir } from "node:os"
16
+ import { startServer } from "../server/server"
17
+ import { pairRunner } from "./runner-pair"
18
+ import { readRunnerCredential } from "./runner-credential"
19
+
20
+ type StartedServer = Awaited<ReturnType<typeof startServer>>
21
+
22
+ async function post(port: number, pathname: string, body?: unknown): Promise<Response> {
23
+ return fetch(`http://127.0.0.1:${port}${pathname}`, {
24
+ method: "POST",
25
+ headers: body !== undefined ? { "Content-Type": "application/json" } : {},
26
+ body: body !== undefined ? JSON.stringify(body) : undefined,
27
+ })
28
+ }
29
+
30
+ describe("runner-pair (integration)", () => {
31
+ let started: StartedServer | null = null
32
+ let natsDataDir: string
33
+ let runnerHome: string
34
+ let savedRunnerHome: string | undefined
35
+
36
+ beforeEach(() => {
37
+ natsDataDir = mkdtempSync(join(tmpdir(), "pr2-pair-flow-nats-"))
38
+ runnerHome = mkdtempSync(join(tmpdir(), "pr2-pair-flow-home-"))
39
+ savedRunnerHome = process.env.TINKARIA_RUNNER_HOME
40
+ process.env.TINKARIA_RUNNER_HOME = runnerHome
41
+ })
42
+
43
+ afterEach(async () => {
44
+ await started?.stop()
45
+ started = null
46
+ rmSync(natsDataDir, { recursive: true, force: true })
47
+ rmSync(runnerHome, { recursive: true, force: true })
48
+ if (savedRunnerHome === undefined) {
49
+ delete process.env.TINKARIA_RUNNER_HOME
50
+ } else {
51
+ process.env.TINKARIA_RUNNER_HOME = savedRunnerHome
52
+ }
53
+ delete process.env.NATS_AUTH_MODE
54
+ delete process.env.NATS_DATA_DIR
55
+ })
56
+
57
+ test("pairRunner: exchange succeeds and credential is written", async () => {
58
+ process.env.NATS_AUTH_MODE = "callout"
59
+ process.env.NATS_DATA_DIR = natsDataDir
60
+ started = await startServer({ port: 4391, host: "127.0.0.1", strictPort: true })
61
+ const port = started.port
62
+
63
+ // Issue a pairing code via the server.
64
+ const issueRes = await post(port, "/api/pairing/code")
65
+ expect(issueRes.status).toBe(200)
66
+ const { code } = await issueRes.json() as { code: string }
67
+
68
+ // Run the pair flow.
69
+ await pairRunner({ serverUrl: `http://127.0.0.1:${port}`, code })
70
+
71
+ // Credential file should now exist and have the right shape.
72
+ const cred = await readRunnerCredential()
73
+ expect(cred).not.toBeNull()
74
+ expect(cred!.runnerId).toMatch(/^runner-/)
75
+ expect(typeof cred!.token).toBe("string")
76
+ expect(typeof cred!.natsUrl).toBe("string")
77
+ expect(typeof cred!.natsWsUrl).toBe("string")
78
+ expect(typeof cred!.pairedAt).toBe("number")
79
+ // Runner connects via the server's /nats-ws proxy (tunneled HTTP port),
80
+ // derived from the paired server URL — not the raw NATS TCP port.
81
+ expect(cred!.natsWsProxyUrl).toBe(`ws://127.0.0.1:${port}/nats-ws`)
82
+ }, 30_000)
83
+
84
+ test("pairRunner: consumed code throws with status 410", async () => {
85
+ process.env.NATS_AUTH_MODE = "callout"
86
+ process.env.NATS_DATA_DIR = natsDataDir
87
+ started = await startServer({ port: 4392, host: "127.0.0.1", strictPort: true })
88
+ const port = started.port
89
+
90
+ const { code } = await post(port, "/api/pairing/code").then((r) => r.json()) as { code: string }
91
+ // First exchange succeeds.
92
+ await pairRunner({ serverUrl: `http://127.0.0.1:${port}`, code })
93
+ // Second exchange of same code should throw (consumed).
94
+ await expect(pairRunner({ serverUrl: `http://127.0.0.1:${port}`, code })).rejects.toThrow(/consumed|410/i)
95
+ }, 30_000)
96
+
97
+ test("pairRunner: unknown code throws with status 400", async () => {
98
+ process.env.NATS_AUTH_MODE = "callout"
99
+ process.env.NATS_DATA_DIR = natsDataDir
100
+ started = await startServer({ port: 4393, host: "127.0.0.1", strictPort: true })
101
+ const port = started.port
102
+
103
+ await expect(
104
+ pairRunner({ serverUrl: `http://127.0.0.1:${port}`, code: "aaaaa-bbbbb-cccccc" })
105
+ ).rejects.toThrow(/unknown|400/i)
106
+ }, 30_000)
107
+ })
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Runner pair flow (PR2 Stage 2).
3
+ *
4
+ * POSTs a pairing code to `POST /api/pairing/exchange` on the given server,
5
+ * then persists the returned credential via writeRunnerCredential.
6
+ *
7
+ * Usage (CLI subcommand wired in runner.ts):
8
+ * bun run src/runner/runner.ts pair --server <url> --code <code>
9
+ */
10
+
11
+ import { writeRunnerCredential } from "./runner-credential"
12
+
13
+ export interface PairRunnerOptions {
14
+ serverUrl: string
15
+ code: string
16
+ }
17
+
18
+ /**
19
+ * Exchange a pairing code for a durable runner credential and write it to disk.
20
+ * Throws if the server returns a non-OK status (expired, consumed, unknown, etc.).
21
+ */
22
+ export async function pairRunner({ serverUrl, code }: PairRunnerOptions): Promise<void> {
23
+ // The exchange RESPONSE carries the durable runner credential. Over plain HTTP
24
+ // to a non-loopback host, a network-path attacker could capture it. Loopback /
25
+ // WireGuard tailnet links are encrypted at the network layer; warn otherwise.
26
+ // (Hard TLS requirement for non-loopback pairing is a pre-multi-tenant gate — decision 0008.)
27
+ try {
28
+ const u = new URL(serverUrl)
29
+ const loopback = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "::1"
30
+ if (u.protocol !== "https:" && !loopback) {
31
+ console.warn(
32
+ `[tinkaria] WARNING: pairing over non-loopback HTTP (${u.host}) — the runner credential is sent in the clear. Use HTTPS or pair over a WireGuard/tailnet link.`
33
+ )
34
+ }
35
+ } catch {
36
+ // malformed URL — let fetch surface the error below
37
+ }
38
+
39
+ const url = `${serverUrl.replace(/\/$/, "")}/api/pairing/exchange`
40
+ const res = await fetch(url, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ code }),
44
+ })
45
+
46
+ if (!res.ok) {
47
+ let errorDetail: string
48
+ try {
49
+ const body = await res.json() as { error?: string }
50
+ errorDetail = body.error ?? res.statusText
51
+ } catch {
52
+ errorDetail = res.statusText
53
+ }
54
+ throw new Error(`Pairing exchange failed (${res.status}): ${errorDetail}`)
55
+ }
56
+
57
+ const { runnerId, token, natsUrl, natsWsUrl } = await res.json() as {
58
+ runnerId: string
59
+ token: string
60
+ natsUrl: string
61
+ natsWsUrl: string
62
+ }
63
+
64
+ // Derive the server's /nats-ws proxy URL from the server we paired against, so
65
+ // the runner connects over the SAME tunneled HTTP port as the browser (e.g.
66
+ // https://host -> wss://host/nats-ws) instead of the raw NATS TCP port, which
67
+ // is often not tunneled and can drop server->runner pushes over the tailnet.
68
+ let natsWsProxyUrl: string | undefined
69
+ try {
70
+ const u = new URL(serverUrl)
71
+ const wsProtocol = u.protocol === "https:" ? "wss:" : "ws:"
72
+ natsWsProxyUrl = `${wsProtocol}//${u.host}/nats-ws`
73
+ } catch {
74
+ // malformed server URL — leave undefined; runner falls back to natsUrl (TCP)
75
+ }
76
+
77
+ await writeRunnerCredential({ runnerId, token, natsUrl, natsWsUrl, natsWsProxyUrl, pairedAt: Date.now() })
78
+
79
+ console.warn(`[tinkaria] Runner paired — runnerId: ${runnerId}`)
80
+ console.warn(`[tinkaria] Credential written. Start the runner with: bun run src/runner/runner.ts`)
81
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, test, expect, afterEach } from "bun:test"
2
+ import { NatsServer } from "@lagz0ne/nats-embedded"
3
+ import { connect } from "@nats-io/transport-node"
4
+ import { jetstreamManager, RetentionPolicy, StorageType } from "@nats-io/jetstream"
5
+ import { Kvm } from "@nats-io/kv"
6
+ import { mkdtempSync, rmSync } from "node:fs"
7
+ import { join } from "node:path"
8
+ import { tmpdir } from "node:os"
9
+ import {
10
+ runnerCmdSubject,
11
+ RUNNER_EVENTS_STREAM,
12
+ ALL_RUNNER_EVENTS,
13
+ RUNNER_REGISTRY_BUCKET,
14
+ type RunnerRegistration,
15
+ } from "../shared/runner-protocol"
16
+
17
+ const encoder = new TextEncoder()
18
+ const decoder = new TextDecoder()
19
+
20
+ describe("runner process", () => {
21
+ let server: NatsServer
22
+ let proc: ReturnType<typeof Bun.spawn> | null = null
23
+ let tmpDir: string | null = null
24
+
25
+ afterEach(async () => {
26
+ if (proc) {
27
+ proc.kill("SIGTERM")
28
+ await proc.exited
29
+ proc = null
30
+ }
31
+ await server?.stop()
32
+ if (tmpDir) {
33
+ rmSync(tmpDir, { recursive: true, force: true })
34
+ tmpDir = null
35
+ }
36
+ })
37
+
38
+ test("starts, registers in KV, and accepts start_turn commands", async () => {
39
+ tmpDir = mkdtempSync(join(tmpdir(), "runner-test-"))
40
+ server = await NatsServer.start({ jetstream: true, storeDir: tmpDir })
41
+ const nc = await connect({ servers: server.url })
42
+
43
+ // Create required stream
44
+ const jsm = await jetstreamManager(nc)
45
+ await jsm.streams.add({
46
+ name: RUNNER_EVENTS_STREAM,
47
+ subjects: [ALL_RUNNER_EVENTS],
48
+ retention: RetentionPolicy.Limits,
49
+ storage: StorageType.File,
50
+ max_age: 5 * 60 * 1_000_000_000,
51
+ max_msgs: 10_000,
52
+ max_bytes: 64 * 1024 * 1024,
53
+ })
54
+
55
+ const runnerId = `test-runner-${Date.now()}`
56
+ proc = Bun.spawn(["bun", "run", "src/runner/runner.ts"], {
57
+ env: {
58
+ ...process.env,
59
+ NATS_URL: server.url,
60
+ RUNNER_ID: runnerId,
61
+ },
62
+ stdio: ["ignore", "pipe", "inherit"],
63
+ })
64
+
65
+ // Wait for runner to start and register
66
+ await new Promise((r) => setTimeout(r, 1000))
67
+
68
+ // Verify KV registration
69
+ const kvm = new Kvm(nc)
70
+ const kvStore = await kvm.open(RUNNER_REGISTRY_BUCKET)
71
+ const entry = await kvStore.get(runnerId)
72
+ expect(entry).toBeDefined()
73
+ const reg = JSON.parse(decoder.decode(entry!.value)) as RunnerRegistration
74
+ expect(reg.runnerId).toBe(runnerId)
75
+
76
+ // Verify it responds to commands (start_turn will fail since there's no real Claude API key, but it should respond)
77
+ const reply = await nc.request(
78
+ runnerCmdSubject(runnerId, "start_turn"),
79
+ encoder.encode(JSON.stringify({
80
+ chatId: "test-chat",
81
+ provider: "claude",
82
+ content: "test",
83
+ model: "test-model",
84
+ planMode: false,
85
+ appendUserPrompt: true,
86
+ workspaceLocalPath: "/tmp",
87
+ sessionToken: null,
88
+ chatTitle: "Test",
89
+ existingMessageCount: 0,
90
+ workspaceId: "p1",
91
+ })),
92
+ { timeout: 5000 }
93
+ )
94
+ const response = JSON.parse(decoder.decode(reply.data))
95
+ // The start_turn will succeed (the turn factory will fail, but the command handler responds ok before the turn runs)
96
+ expect(response).toHaveProperty("ok")
97
+
98
+ await nc.drain()
99
+ })
100
+
101
+ test("shuts down cleanly on SIGTERM", async () => {
102
+ tmpDir = mkdtempSync(join(tmpdir(), "runner-test-"))
103
+ server = await NatsServer.start({ jetstream: true, storeDir: tmpDir })
104
+ const nc = await connect({ servers: server.url })
105
+
106
+ const jsm = await jetstreamManager(nc)
107
+ await jsm.streams.add({
108
+ name: RUNNER_EVENTS_STREAM,
109
+ subjects: [ALL_RUNNER_EVENTS],
110
+ retention: RetentionPolicy.Limits,
111
+ storage: StorageType.File,
112
+ max_age: 5 * 60 * 1_000_000_000,
113
+ max_msgs: 10_000,
114
+ max_bytes: 64 * 1024 * 1024,
115
+ })
116
+
117
+ proc = Bun.spawn(["bun", "run", "src/runner/runner.ts"], {
118
+ env: {
119
+ ...process.env,
120
+ NATS_URL: server.url,
121
+ RUNNER_ID: "runner-sigterm-test",
122
+ },
123
+ stdio: ["ignore", "pipe", "inherit"],
124
+ })
125
+
126
+ await new Promise((r) => setTimeout(r, 500))
127
+
128
+ proc.kill("SIGTERM")
129
+ const exitCode = await proc.exited
130
+ expect(exitCode).toBe(0)
131
+ proc = null
132
+
133
+ await nc.drain()
134
+ })
135
+ })