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,243 @@
1
+ import type {
2
+ ChatPermissionPolicy,
3
+ PolicyVerdict,
4
+ } from "../../shared/permission-policy"
5
+ import { parse as shellParse } from "shell-quote"
6
+ import path from "node:path"
7
+ import { homedir } from "node:os"
8
+ import { minimatch } from "minimatch"
9
+
10
+ export interface EvaluateArgs {
11
+ toolName: string
12
+ args: Record<string, unknown>
13
+ chatPolicy: ChatPermissionPolicy
14
+ cwd: string
15
+ }
16
+
17
+ export interface EvaluateResult {
18
+ verdict: PolicyVerdict
19
+ reason?: string
20
+ }
21
+
22
+ function argsToText(args: Record<string, unknown>): string {
23
+ return typeof args.command === "string" ? args.command : JSON.stringify(args)
24
+ }
25
+
26
+ interface ShellOp { op: string }
27
+ function isShellOp(token: unknown): token is ShellOp {
28
+ return typeof token === "object" && token !== null && "op" in (token as object)
29
+ }
30
+
31
+ interface ParsedSimpleCommand {
32
+ verb: string
33
+ paths: string[]
34
+ hadEnvPrefix: boolean
35
+ }
36
+
37
+ function parseSimpleBash(
38
+ command: string,
39
+ cwd: string,
40
+ autoAllowVerbs: string[],
41
+ ): ParsedSimpleCommand | null {
42
+ const tokens = shellParse(command)
43
+ for (const t of tokens) {
44
+ if (isShellOp(t)) return null // pipe/redirect/subshell/glob/etc.
45
+ }
46
+ const stringTokens = tokens.filter((t): t is string => typeof t === "string")
47
+ if (stringTokens.length === 0) return null
48
+
49
+ let hadEnvPrefix = false
50
+ let i = 0
51
+ while (i < stringTokens.length && /^[A-Z_][A-Z0-9_]*=/.test(stringTokens[i])) {
52
+ hadEnvPrefix = true
53
+ i++
54
+ }
55
+ const rest = stringTokens.slice(i)
56
+ if (rest.length === 0) return null
57
+
58
+ let verb: string | null = null
59
+ let argsStart = 1
60
+ const sorted = [...autoAllowVerbs].sort((a, b) => b.length - a.length)
61
+ for (const candidate of sorted) {
62
+ const parts = candidate.split(/\s+/)
63
+ if (rest.length >= parts.length && parts.every((p, idx) => rest[idx] === p)) {
64
+ verb = candidate
65
+ argsStart = parts.length
66
+ break
67
+ }
68
+ }
69
+ if (!verb) {
70
+ verb = rest[0]
71
+ argsStart = 1
72
+ }
73
+
74
+ const paths: string[] = []
75
+ for (const arg of rest.slice(argsStart)) {
76
+ const isPathLike = arg.startsWith("~") || arg.includes("/") || arg.startsWith(".")
77
+ if (!isPathLike) continue
78
+ const expanded = arg.startsWith("~")
79
+ ? path.join(homedir(), arg.slice(1).replace(/^\//, ""))
80
+ : arg
81
+ const resolved = path.resolve(cwd, expanded)
82
+ paths.push(resolved)
83
+ }
84
+ return { verb, paths, hadEnvPrefix }
85
+ }
86
+
87
+ const READ_PATH_TOOLS = new Set([
88
+ "mcp__kanna__read",
89
+ "mcp__kanna__glob",
90
+ "mcp__kanna__grep",
91
+ ])
92
+ const WRITE_PATH_TOOLS = new Set([
93
+ "mcp__kanna__write",
94
+ "mcp__kanna__edit",
95
+ ])
96
+
97
+ function getPathArg(args: Record<string, unknown>): string | null {
98
+ if (typeof args.path === "string") return args.path
99
+ return null
100
+ }
101
+
102
+ function pathMatchesDeny(absPath: string, deny: string[]): string | null {
103
+ for (const pattern of deny) {
104
+ let expanded = pattern.startsWith("~")
105
+ ? path.join(homedir(), pattern.slice(1).replace(/^\//, ""))
106
+ : pattern
107
+ // Normalize trailing slash so "/some/dir/" matches the same as "/some/dir"
108
+ if (expanded.endsWith("/") && expanded !== "/") expanded = expanded.slice(0, -1)
109
+ const matchPattern = expanded.endsWith("/**") || expanded.includes("*")
110
+ ? expanded
111
+ : `${expanded}/**`
112
+ if (minimatch(absPath, matchPattern, { dot: true }) || absPath === expanded) {
113
+ return pattern
114
+ }
115
+ }
116
+ return null
117
+ }
118
+
119
+ /**
120
+ * Tools whose entire purpose is to surface a question / plan to the user
121
+ * and wait for an answer. They MUST always go through the "ask" path so
122
+ * the durable approval protocol renders UI and the model receives the
123
+ * user's actual response. Auto-allow/auto-deny would resolve the request
124
+ * with no payload, leaving the shim's `formatAnswer` with an undefined
125
+ * payload — producing an empty `text` field and an MCP -32602
126
+ * "Invalid tools/call result" validation error (issue #215 follow-up).
127
+ * No `chatPolicy.defaultAction` value can override this.
128
+ */
129
+ const INTERACTIVE_TOOLS = new Set([
130
+ "mcp__kanna__ask_user_question",
131
+ "mcp__kanna__exit_plan_mode",
132
+ ])
133
+
134
+ export const policy = {
135
+ evaluate(args: EvaluateArgs): EvaluateResult {
136
+ if (INTERACTIVE_TOOLS.has(args.toolName)) {
137
+ return { verdict: "ask", reason: "interactive tool: always asks the user" }
138
+ }
139
+ if (READ_PATH_TOOLS.has(args.toolName)) {
140
+ const p = getPathArg(args.args)
141
+ if (p !== null) {
142
+ const expanded = p.startsWith("~")
143
+ ? path.join(homedir(), p.slice(1).replace(/^\//, ""))
144
+ : p
145
+ const resolved = path.resolve(args.cwd, expanded)
146
+ const denied = pathMatchesDeny(resolved, args.chatPolicy.readPathDeny)
147
+ if (denied) {
148
+ return { verdict: "auto-deny", reason: `readPathDeny: ${denied}` }
149
+ }
150
+ }
151
+ }
152
+ if (WRITE_PATH_TOOLS.has(args.toolName)) {
153
+ const p = getPathArg(args.args)
154
+ if (p !== null) {
155
+ const expanded = p.startsWith("~")
156
+ ? path.join(homedir(), p.slice(1).replace(/^\//, ""))
157
+ : p
158
+ const resolved = path.resolve(args.cwd, expanded)
159
+ const deniedW = pathMatchesDeny(resolved, args.chatPolicy.writePathDeny)
160
+ const deniedR = pathMatchesDeny(resolved, args.chatPolicy.readPathDeny)
161
+ if (deniedW) return { verdict: "auto-deny", reason: `writePathDeny: ${deniedW}` }
162
+ if (deniedR) return { verdict: "auto-deny", reason: `readPathDeny: ${deniedR}` }
163
+ }
164
+ }
165
+
166
+ // Bash path: single block handles all bash decisions.
167
+ // Path-deny + tool-deny always run; the only thing chatPolicy.defaultAction
168
+ // changes is the fallback for "didn't hit a deny rule and didn't hit the
169
+ // verb allowlist". For personal-use (defaultAction: auto-allow) that's
170
+ // auto-allow; for shared sessions (defaultAction: ask) it's ask.
171
+ if (args.toolName === "mcp__kanna__bash") {
172
+ const command = typeof args.args.command === "string" ? args.args.command : ""
173
+ const parsed = parseSimpleBash(command, args.cwd, args.chatPolicy.bash.autoAllowVerbs)
174
+ const fallback: PolicyVerdict = args.chatPolicy.defaultAction === "ask"
175
+ ? "ask"
176
+ : args.chatPolicy.defaultAction
177
+ // Deny-list applies regardless of shell-feature parsing.
178
+ for (const rule of args.chatPolicy.toolDenyList) {
179
+ if (rule.tool !== args.toolName) continue
180
+ let re: RegExp
181
+ try {
182
+ re = new RegExp(rule.pattern)
183
+ } catch {
184
+ console.warn(`[permission-gate] invalid regex pattern: ${rule.pattern}`)
185
+ continue
186
+ }
187
+ if (re.test(argsToText(args.args))) {
188
+ return { verdict: "auto-deny", reason: `matched denylist: ${rule.pattern}` }
189
+ }
190
+ }
191
+ if (!parsed) {
192
+ return { verdict: fallback, reason: "bash command uses shell features" }
193
+ }
194
+ if (parsed.hadEnvPrefix) {
195
+ return { verdict: fallback, reason: "bash command has env prefix" }
196
+ }
197
+ for (const p of parsed.paths) {
198
+ const denied = pathMatchesDeny(p, args.chatPolicy.readPathDeny)
199
+ if (denied) {
200
+ return { verdict: "auto-deny", reason: `readPathDeny: ${denied}` }
201
+ }
202
+ }
203
+ if (args.chatPolicy.bash.autoAllowVerbs.includes(parsed.verb)) {
204
+ return { verdict: "auto-allow", reason: `verb in autoAllowVerbs: ${parsed.verb}` }
205
+ }
206
+ return { verdict: fallback, reason: "bash verb not on autoAllowVerbs" }
207
+ }
208
+
209
+ // Non-bash path: deny-list, allow-list, default.
210
+ // 1. Deny list wins over everything.
211
+ for (const rule of args.chatPolicy.toolDenyList) {
212
+ if (rule.tool !== args.toolName) continue
213
+ let re: RegExp
214
+ try {
215
+ re = new RegExp(rule.pattern)
216
+ } catch {
217
+ console.warn(`[permission-gate] invalid regex pattern: ${rule.pattern}`)
218
+ continue
219
+ }
220
+ if (re.test(argsToText(args.args))) {
221
+ return { verdict: "auto-deny", reason: `matched denylist: ${rule.pattern}` }
222
+ }
223
+ }
224
+
225
+ // 2. Allow list
226
+ for (const rule of args.chatPolicy.toolAllowList) {
227
+ if (rule.tool !== args.toolName) continue
228
+ let re: RegExp
229
+ try {
230
+ re = new RegExp(rule.pattern)
231
+ } catch {
232
+ console.warn(`[permission-gate] invalid regex pattern: ${rule.pattern}`)
233
+ continue
234
+ }
235
+ if (re.test(argsToText(args.args))) {
236
+ return { verdict: "auto-allow", reason: `matched allowlist: ${rule.pattern}` }
237
+ }
238
+ }
239
+
240
+ // 4. Default action.
241
+ return { verdict: args.chatPolicy.defaultAction === "ask" ? "ask" : args.chatPolicy.defaultAction }
242
+ },
243
+ }
@@ -0,0 +1,107 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import process from "node:process"
4
+
5
+ export interface TerminalPidEntry {
6
+ terminalId: string
7
+ pid: number
8
+ cwd: string
9
+ createdAt: number
10
+ }
11
+
12
+ interface RegistryFile {
13
+ entries: TerminalPidEntry[]
14
+ }
15
+
16
+ export class TerminalPidRegistry {
17
+ private readonly filePath: string
18
+ private entries: TerminalPidEntry[] = []
19
+ private writeQueue: Promise<void> = Promise.resolve()
20
+
21
+ constructor(filePath: string) {
22
+ this.filePath = filePath
23
+ }
24
+
25
+ async register(entry: Omit<TerminalPidEntry, "createdAt">): Promise<void> {
26
+ await this.loadIfNeeded()
27
+ const next = this.entries.filter((existing) => existing.terminalId !== entry.terminalId)
28
+ next.push({ ...entry, createdAt: Date.now() })
29
+ this.entries = next
30
+ await this.persist()
31
+ }
32
+
33
+ async unregister(terminalId: string): Promise<void> {
34
+ await this.loadIfNeeded()
35
+ this.entries = this.entries.filter((entry) => entry.terminalId !== terminalId)
36
+ await this.persist()
37
+ }
38
+
39
+ async reapStale(): Promise<TerminalPidEntry[]> {
40
+ const stored = await this.readFromDisk()
41
+ if (stored.length === 0) {
42
+ this.entries = []
43
+ return []
44
+ }
45
+ for (const entry of stored) {
46
+ killPgroup(entry.pid)
47
+ }
48
+ this.entries = []
49
+ await this.persist()
50
+ return stored
51
+ }
52
+
53
+ private async loadIfNeeded() {
54
+ if (this.entries.length > 0) return
55
+ this.entries = await this.readFromDisk()
56
+ }
57
+
58
+ private async readFromDisk(): Promise<TerminalPidEntry[]> {
59
+ let raw: string
60
+ try {
61
+ raw = await readFile(this.filePath, "utf8")
62
+ } catch {
63
+ return []
64
+ }
65
+ try {
66
+ const parsed = JSON.parse(raw) as Partial<RegistryFile>
67
+ if (!parsed || !Array.isArray(parsed.entries)) return []
68
+ return parsed.entries.filter(isValidEntry)
69
+ } catch {
70
+ return []
71
+ }
72
+ }
73
+
74
+ private async persist() {
75
+ const snapshot: RegistryFile = { entries: [...this.entries] }
76
+ const serialized = JSON.stringify(snapshot)
77
+ this.writeQueue = this.writeQueue
78
+ .catch(() => undefined)
79
+ .then(async () => {
80
+ await mkdir(path.dirname(this.filePath), { recursive: true })
81
+ await writeFile(this.filePath, serialized, "utf8")
82
+ })
83
+ await this.writeQueue
84
+ }
85
+ }
86
+
87
+ function isValidEntry(value: unknown): value is TerminalPidEntry {
88
+ if (!value || typeof value !== "object") return false
89
+ const candidate = value as Partial<TerminalPidEntry>
90
+ return (
91
+ typeof candidate.terminalId === "string"
92
+ && typeof candidate.pid === "number"
93
+ && Number.isFinite(candidate.pid)
94
+ && typeof candidate.cwd === "string"
95
+ && typeof candidate.createdAt === "number"
96
+ )
97
+ }
98
+
99
+ function killPgroup(pid: number) {
100
+ if (process.platform === "win32") return
101
+ if (!Number.isFinite(pid) || pid <= 0) return
102
+ try {
103
+ process.kill(-pid, "SIGKILL")
104
+ } catch {
105
+ // ESRCH (already gone) and EPERM (race with kernel reap) are fine.
106
+ }
107
+ }
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { POLICY_DEFAULT } from "../../../shared/permission-policy"
6
+ import { createToolCallbackService } from "../../claude-pty/tool-callback"
7
+ import { createTestEventStore } from "../../storage/test-helpers"
8
+ import { createAskUserQuestionTool } from "./ask-user-question"
9
+
10
+ async function newStore() {
11
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-mcp-aq-"))
12
+ const store = createTestEventStore(dir)
13
+ await store.initialize()
14
+ const cleanup = async () => {
15
+ await new Promise<void>((r) => setTimeout(r, 50))
16
+ await rm(dir, { recursive: true, force: true })
17
+ }
18
+ return { store, dir, cleanup }
19
+ }
20
+
21
+ const handlerCtx = () => ({
22
+ chatId: "c1",
23
+ sessionId: "s1",
24
+ toolUseId: "tu1",
25
+ cwd: "/tmp",
26
+ chatPolicy: POLICY_DEFAULT,
27
+ })
28
+
29
+ describe("mcp__kanna__ask_user_question", () => {
30
+ test("calls policy.evaluate then routes to tool-callback", async () => {
31
+ const { store, cleanup } = await newStore()
32
+ try {
33
+ const svc = createToolCallbackService({
34
+ store, serverSecret: "k", now: () => 1, timeoutMs: 600_000,
35
+ })
36
+ const tool = createAskUserQuestionTool({ toolCallback: svc })
37
+ const inputArgs = {
38
+ questions: [{
39
+ text: "ok?",
40
+ header: "OK",
41
+ options: [{ label: "yes", description: "" }, { label: "no", description: "" }],
42
+ multiSelect: false,
43
+ }],
44
+ }
45
+ const promise = tool.handler(inputArgs, handlerCtx())
46
+ const pending = await store.listPendingToolRequests("c1")
47
+ expect(pending).toHaveLength(1)
48
+ await svc.answer(pending[0].id, { kind: "answer", payload: { answers: { "ok?": "yes" } } })
49
+ const result = await promise
50
+ expect(result.content[0].type).toBe("text")
51
+ expect(JSON.parse(result.content[0].text).answers).toEqual({ "ok?": "yes" })
52
+ expect(result.isError).toBeFalsy()
53
+ } finally { await cleanup() }
54
+ })
55
+
56
+ // Issue #215 follow-up: even under chatPolicy.defaultAction "auto-deny"
57
+ // the tool must take the ask path (UI is the only meaningful outcome),
58
+ // then deny only when the user / cancel resolves it that way.
59
+ test("auto-deny chatPolicy still routes through ask (UI), denial only via cancel/cancelAllForChat", async () => {
60
+ const { store, cleanup } = await newStore()
61
+ try {
62
+ const svc = createToolCallbackService({
63
+ store, serverSecret: "k", now: () => 1, timeoutMs: 600_000,
64
+ })
65
+ const tool = createAskUserQuestionTool({ toolCallback: svc })
66
+ const promise = tool.handler(
67
+ {
68
+ questions: [{
69
+ text: "x",
70
+ header: "X",
71
+ options: [{ label: "a", description: "" }, { label: "b", description: "" }],
72
+ multiSelect: false,
73
+ }],
74
+ },
75
+ { ...handlerCtx(), chatPolicy: { ...POLICY_DEFAULT, defaultAction: "auto-deny" } },
76
+ )
77
+ const pending = await store.listPendingToolRequests("c1")
78
+ expect(pending).toHaveLength(1)
79
+ await svc.cancelAllForChat("c1", "user-cancel")
80
+ const result = await promise
81
+ expect(result.isError).toBe(true)
82
+ expect(result.content[0].type).toBe("text")
83
+ expect(typeof result.content[0].text).toBe("string")
84
+ } finally { await cleanup() }
85
+ })
86
+
87
+ // Issue #215 follow-up: fail fast on empty/undefined answer payload.
88
+ // The earlier bug silently produced `text: JSON.stringify(undefined)` =
89
+ // `text: undefined` and crashed the MCP SDK validator with -32602.
90
+ // Coercing to `{}` would hide the underlying policy-gate bug (interactive
91
+ // tool auto-allowed without user input). Instead the shim throws so the
92
+ // failure is loud and the root cause is detectable.
93
+ test("answer decision with no payload → throws loudly (no silent {} coercion)", async () => {
94
+ const { store, cleanup } = await newStore()
95
+ try {
96
+ const svc = createToolCallbackService({
97
+ store, serverSecret: "k", now: () => 1, timeoutMs: 600_000,
98
+ })
99
+ const tool = createAskUserQuestionTool({ toolCallback: svc })
100
+ const promise = tool.handler(
101
+ {
102
+ questions: [{
103
+ text: "ok?",
104
+ header: "OK",
105
+ options: [{ label: "yes", description: "" }, { label: "no", description: "" }],
106
+ multiSelect: false,
107
+ }],
108
+ },
109
+ handlerCtx(),
110
+ )
111
+ const pending = await store.listPendingToolRequests("c1")
112
+ expect(pending).toHaveLength(1)
113
+ // Resolve with kind:"allow" and no payload — the exact auto-allow
114
+ // shape that previously slipped through.
115
+ await svc.answer(pending[0].id, { kind: "allow" as const })
116
+ await expect(promise).rejects.toThrow(/empty answer payload/i)
117
+ } finally { await cleanup() }
118
+ })
119
+ })
@@ -0,0 +1,61 @@
1
+ import { z } from "zod"
2
+ import type { ToolCallbackService } from "../../claude-pty/tool-callback"
3
+ import type { ToolHandlerContext, ToolHandlerResult } from "./tool-callback-shim"
4
+ import { gatedToolCall } from "./tool-callback-shim"
5
+
6
+ const QuestionSchema = z.object({
7
+ text: z.string(),
8
+ header: z.string(),
9
+ options: z.array(z.object({ label: z.string(), description: z.string() })).min(2).max(4),
10
+ multiSelect: z.boolean(),
11
+ })
12
+
13
+ const InputSchema = z.object({
14
+ questions: z.array(QuestionSchema).min(1).max(4),
15
+ })
16
+
17
+ export type AskUserQuestionInput = z.infer<typeof InputSchema>
18
+
19
+ export interface AskUserQuestionTool {
20
+ name: "ask_user_question"
21
+ schema: typeof InputSchema
22
+ handler: (input: AskUserQuestionInput, ctx: ToolHandlerContext) => Promise<ToolHandlerResult>
23
+ }
24
+
25
+ export function createAskUserQuestionTool(deps: { toolCallback: ToolCallbackService }): AskUserQuestionTool {
26
+ return {
27
+ name: "ask_user_question",
28
+ schema: InputSchema,
29
+ async handler(input, ctx) {
30
+ return gatedToolCall({
31
+ toolCallback: deps.toolCallback,
32
+ toolName: "mcp__kanna__ask_user_question",
33
+ ctx,
34
+ args: input as unknown as Record<string, unknown>,
35
+ formatAnswer: (payload) => {
36
+ // Fail fast — silently coercing an undefined payload to `{}` would
37
+ // hide the real bug (an interactive tool being auto-allowed with
38
+ // no user answer). The policy gate is supposed to force "ask" for
39
+ // this tool (issue #215 follow-up); if we ever see an allow/answer
40
+ // with no payload here, surface it loudly so it gets reported and
41
+ // fixed instead of producing a silent empty UI answer downstream.
42
+ if (payload === undefined || payload === null || typeof payload !== "object") {
43
+ throw new Error(
44
+ "mcp__kanna__ask_user_question: empty answer payload "
45
+ + `(received ${payload === undefined ? "undefined" : typeof payload}). `
46
+ + "This means the policy gate or tool-callback resolved the request without a user response — "
47
+ + "interactive tools must always go through the ask/UI path. See issue #215.",
48
+ )
49
+ }
50
+ return {
51
+ content: [{ type: "text" as const, text: JSON.stringify(payload) }],
52
+ }
53
+ },
54
+ formatDeny: (reason) => ({
55
+ content: [{ type: "text" as const, text: `Denied: ${reason}` }],
56
+ isError: true,
57
+ }),
58
+ })
59
+ },
60
+ }
61
+ }
@@ -0,0 +1,76 @@
1
+ import { z } from "zod"
2
+ import type { ToolCallbackService } from "../../claude-pty/tool-callback"
3
+ import type { ToolHandlerContext, ToolHandlerResult } from "./tool-callback-shim"
4
+ import { gatedToolCall } from "./tool-callback-shim"
5
+
6
+ const InputSchema = z.object({
7
+ command: z.string(),
8
+ })
9
+
10
+ export type BashInput = z.infer<typeof InputSchema>
11
+
12
+ export interface BashTool {
13
+ name: "bash"
14
+ schema: typeof InputSchema
15
+ handler: (input: BashInput, ctx: ToolHandlerContext) => Promise<ToolHandlerResult>
16
+ }
17
+
18
+ const OUTPUT_CAP = 1_000_000 // 1 MB per stream
19
+
20
+ async function readBounded(stream: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<string> {
21
+ if (!stream) return ""
22
+ const reader = stream.getReader()
23
+ const chunks: Uint8Array[] = []
24
+ let total = 0
25
+ while (total < maxBytes) {
26
+ const { value, done } = await reader.read()
27
+ if (done) break
28
+ chunks.push(value)
29
+ total += value.byteLength
30
+ }
31
+ reader.cancel().catch(() => {})
32
+ let text = Buffer.concat(chunks.map(c => Buffer.from(c))).toString("utf8")
33
+ if (total >= maxBytes) text += "\n\n[output truncated at 1 MB]"
34
+ return text
35
+ }
36
+
37
+ export function createBashTool(deps: { toolCallback: ToolCallbackService }): BashTool {
38
+ return {
39
+ name: "bash",
40
+ schema: InputSchema,
41
+ async handler(input, ctx) {
42
+ return gatedToolCall({
43
+ toolCallback: deps.toolCallback,
44
+ toolName: "mcp__kanna__bash",
45
+ ctx,
46
+ args: input as unknown as Record<string, unknown>,
47
+ formatAnswer: async () => {
48
+ const proc = Bun.spawn(["/bin/sh", "-c", input.command], {
49
+ cwd: ctx.cwd,
50
+ stdin: "ignore",
51
+ stdout: "pipe",
52
+ stderr: "pipe",
53
+ })
54
+ const [stdoutBuf, stderrBuf, exitCode] = await Promise.all([
55
+ readBounded(proc.stdout, OUTPUT_CAP),
56
+ readBounded(proc.stderr, OUTPUT_CAP),
57
+ proc.exited,
58
+ ])
59
+ const text = [
60
+ stdoutBuf,
61
+ stderrBuf ? `STDERR:\n${stderrBuf}` : "",
62
+ `Exit code: ${exitCode}`,
63
+ ].filter(Boolean).join("\n")
64
+ return {
65
+ content: [{ type: "text" as const, text }],
66
+ isError: exitCode !== 0,
67
+ }
68
+ },
69
+ formatDeny: (reason) => ({
70
+ content: [{ type: "text" as const, text: `Denied: ${reason}` }],
71
+ isError: true,
72
+ }),
73
+ })
74
+ },
75
+ }
76
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { POLICY_DEFAULT } from "../../../shared/permission-policy"
6
+ import { createTestEventStore } from "../../storage/test-helpers"
7
+ import { createToolCallbackService } from "../../claude-pty/tool-callback"
8
+ import { createBashTool } from "./bash.adapter"
9
+
10
+ async function newStore() {
11
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-mcp-bash-"))
12
+ const store = createTestEventStore(dir)
13
+ await store.initialize()
14
+ // Delay before removing dir so background persist tasks (fired by auto-allow/auto-deny)
15
+ // have time to complete before the tmpdir is removed.
16
+ const cleanup = async () => {
17
+ await new Promise<void>((r) => setTimeout(r, 50))
18
+ await rm(dir, { recursive: true, force: true })
19
+ }
20
+ return { store, dir, cleanup }
21
+ }
22
+
23
+ const ctx = (cwd: string) => ({
24
+ chatId: "c",
25
+ sessionId: "s",
26
+ toolUseId: "tu",
27
+ cwd,
28
+ chatPolicy: { ...POLICY_DEFAULT, defaultAction: "auto-allow" as const },
29
+ })
30
+
31
+ describe("mcp__kanna__bash", () => {
32
+ test("auto-allowed verb (pwd) → stdout contains cwd, no isError", async () => {
33
+ const { store, dir, cleanup } = await newStore()
34
+ try {
35
+ const svc = createToolCallbackService({ store, serverSecret: "k", now: () => 1, timeoutMs: 600_000 })
36
+ const tool = createBashTool({ toolCallback: svc })
37
+ const result = await tool.handler({ command: "pwd" }, ctx(dir))
38
+ expect(result.isError).toBeFalsy()
39
+ // pwd resolves symlinks; use realpath comparison
40
+ const realDir = await Bun.spawn(["realpath", dir], { stdout: "pipe" })
41
+ const realDirStr = (await new Response(realDir.stdout).text()).trim()
42
+ const resultText = result.content[0].text
43
+ expect(resultText).toContain(realDirStr)
44
+ } finally { await cleanup() }
45
+ }, 30_000)
46
+
47
+ test("toolDenyList match (rm -rf /) → isError true", async () => {
48
+ const { store, dir, cleanup } = await newStore()
49
+ try {
50
+ const svc = createToolCallbackService({ store, serverSecret: "k", now: () => 1, timeoutMs: 600_000 })
51
+ const tool = createBashTool({ toolCallback: svc })
52
+ const result = await tool.handler({ command: "rm -rf /" }, ctx(dir))
53
+ expect(result.isError).toBe(true)
54
+ } finally { await cleanup() }
55
+ }, 30_000)
56
+ })