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.
- package/LICENSE +84 -0
- package/README.md +202 -0
- package/bin/airaknit +9 -0
- package/bin/airaknit-project +14 -0
- package/bin/kanna +9 -0
- package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
- package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
- package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
- package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
- package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
- package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
- package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
- package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
- package/dist/client/assets/arc-B07zg7ol.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
- package/dist/client/assets/array-BGFCBI0e.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
- package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
- package/dist/client/assets/button-DO50qOGv.js +1 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
- package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
- package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
- package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
- package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
- package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
- package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
- package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
- package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
- package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
- package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
- package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
- package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
- package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
- package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
- package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
- package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
- package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
- package/dist/client/assets/client-BrKWI4CM.js +1 -0
- package/dist/client/assets/client-CGgNRU9w.js +1 -0
- package/dist/client/assets/client-DMSLRzg9.js +6 -0
- package/dist/client/assets/clone-DWcL7whJ.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
- package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
- package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
- package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
- package/dist/client/assets/dist-CuB4kiSK.js +1 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
- package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
- package/dist/client/assets/index-CetCiuqP.js +105 -0
- package/dist/client/assets/index-N29Mip7A.css +1 -0
- package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
- package/dist/client/assets/init-B8gtcn7T.js +1 -0
- package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
- package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
- package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
- package/dist/client/assets/katex-B94qP8b6.js +265 -0
- package/dist/client/assets/lib--QVjyxmL.js +29 -0
- package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
- package/dist/client/assets/line-DCrYfLBn.js +1 -0
- package/dist/client/assets/linear-_4upLmeo.js +1 -0
- package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
- package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
- package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
- package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
- package/dist/client/assets/path-DZF-JdEe.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
- package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
- package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
- package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
- package/dist/client/assets/react-CClhXMB2.js +1 -0
- package/dist/client/assets/react-Dd6D81m0.js +1 -0
- package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
- package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
- package/dist/client/assets/src-DeTlMJAU.js +1 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
- package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +15 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +31 -0
- package/dist/client/manifest.webmanifest +12 -0
- package/dist/client/sw.js +32 -0
- package/package.json +122 -0
- package/src/nats/auth-callout/callout-config.test.ts +93 -0
- package/src/nats/auth-callout/callout-config.ts +109 -0
- package/src/nats/auth-callout/callout.integration.test.ts +332 -0
- package/src/nats/auth-callout/keys.ts +103 -0
- package/src/nats/auth-callout/responder.ts +241 -0
- package/src/nats/auth-callout/scope-policy.test.ts +159 -0
- package/src/nats/auth-callout/scope-policy.ts +210 -0
- package/src/nats/auth-callout/token.test.ts +163 -0
- package/src/nats/auth-callout/token.ts +157 -0
- package/src/nats/nats-daemon-callout.ts +194 -0
- package/src/nats/nats-daemon.test.ts +77 -0
- package/src/nats/nats-daemon.ts +50 -0
- package/src/nats/nats-token.test.ts +61 -0
- package/src/nats/nats-token.ts +59 -0
- package/src/runner/coordination-mcp-integration.test.ts +134 -0
- package/src/runner/nats-coordination-client.test.ts +49 -0
- package/src/runner/nats-coordination-client.ts +94 -0
- package/src/runner/runner-agent.test.ts +469 -0
- package/src/runner/runner-agent.ts +453 -0
- package/src/runner/runner-credential.test.ts +93 -0
- package/src/runner/runner-credential.ts +82 -0
- package/src/runner/runner-nats.test.ts +495 -0
- package/src/runner/runner-nats.ts +323 -0
- package/src/runner/runner-pair.test.ts +107 -0
- package/src/runner/runner-pair.ts +81 -0
- package/src/runner/runner.test.ts +135 -0
- package/src/runner/runner.ts +212 -0
- package/src/runner/turn-factories.test.ts +97 -0
- package/src/runner/turn-factories.ts +475 -0
- package/src/server/agent-config-journey.test.ts +106 -0
- package/src/server/agent.ts +8 -0
- package/src/server/auto-continue/auth-error-detector.ts +66 -0
- package/src/server/auto-continue/limit-detector.ts +194 -0
- package/src/server/bm25.test.ts +92 -0
- package/src/server/bm25.ts +101 -0
- package/src/server/chat-events-jetstream.test.ts +135 -0
- package/src/server/claude-harness.ts +360 -0
- package/src/server/claude-pty/agent-normalizers.ts +309 -0
- package/src/server/claude-pty/auth.test.ts +38 -0
- package/src/server/claude-pty/auth.ts +32 -0
- package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
- package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
- package/src/server/claude-pty/driver.test.ts +902 -0
- package/src/server/claude-pty/driver.ts +807 -0
- package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
- package/src/server/claude-pty/jsonl-path.test.ts +114 -0
- package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
- package/src/server/claude-pty/jsonl-to-event.ts +174 -0
- package/src/server/claude-pty/output-ring.test.ts +35 -0
- package/src/server/claude-pty/output-ring.ts +25 -0
- package/src/server/claude-pty/parity-matrix.test.ts +227 -0
- package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
- package/src/server/claude-pty/pid-registry.test.ts +122 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
- package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
- package/src/server/claude-pty/pty-instance-registry.ts +166 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
- package/src/server/claude-pty/pty-process.adapter.ts +66 -0
- package/src/server/claude-pty/pty-process.test.ts +49 -0
- package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
- package/src/server/claude-pty/resolve-binary.test.ts +118 -0
- package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
- package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
- package/src/server/claude-pty/settings-writer.test.ts +22 -0
- package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
- package/src/server/claude-pty/smoke-test.test.ts +191 -0
- package/src/server/claude-pty/smoke-test.ts +185 -0
- package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
- package/src/server/claude-pty/tool-callback.ts +274 -0
- package/src/server/claude-pty/tui-control.test.ts +272 -0
- package/src/server/claude-pty/tui-control.ts +182 -0
- package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
- package/src/server/claude-pty/tui-source.adapter.ts +343 -0
- package/src/server/claude-pty/tunnel-gateway.ts +12 -0
- package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
- package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
- package/src/server/claude-pty-mcp/history-primer.ts +90 -0
- package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
- package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
- package/src/server/claude-pty-mcp/mcp.ts +412 -0
- package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
- package/src/server/claude-pty-mcp/paths.ts +24 -0
- package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
- package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
- package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
- package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
- package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
- package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
- package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
- package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
- package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
- package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
- package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
- package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
- package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
- package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
- package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
- package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
- package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
- package/src/server/claude-pty-mcp/uploads.ts +38 -0
- package/src/server/claude-turn.test.ts +176 -0
- package/src/server/cli-runtime.test.ts +456 -0
- package/src/server/cli-runtime.ts +374 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +78 -0
- package/src/server/client-log-forwarder.test.ts +74 -0
- package/src/server/client-log-forwarder.ts +75 -0
- package/src/server/codex-app-server-protocol.ts +449 -0
- package/src/server/codex-app-server.test.ts +2990 -0
- package/src/server/codex-app-server.ts +1713 -0
- package/src/server/coordination-integration.test.ts +63 -0
- package/src/server/coordination-mcp.test.ts +149 -0
- package/src/server/coordination-mcp.ts +197 -0
- package/src/server/delegation-coordinator.test.ts +675 -0
- package/src/server/delegation-coordinator.ts +454 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store-agent-config.test.ts +124 -0
- package/src/server/event-store-coordination.test.ts +149 -0
- package/src/server/event-store-profile.test.ts +132 -0
- package/src/server/event-store-repo.test.ts +154 -0
- package/src/server/event-store-runner-team.test.ts +104 -0
- package/src/server/event-store.test.ts +342 -0
- package/src/server/event-store.ts +2208 -0
- package/src/server/events.ts +379 -0
- package/src/server/extension-router.test.ts +183 -0
- package/src/server/extension-router.ts +114 -0
- package/src/server/extensions/agents/server.test.ts +191 -0
- package/src/server/extensions/agents/server.ts +108 -0
- package/src/server/extensions/c3/server.test.ts +284 -0
- package/src/server/extensions/c3/server.ts +212 -0
- package/src/server/extensions/code/server.test.ts +200 -0
- package/src/server/extensions/code/server.ts +150 -0
- package/src/server/extensions.config.ts +10 -0
- package/src/server/external-open.ts +69 -0
- package/src/server/generate-fork-context.ts +58 -0
- package/src/server/generate-merge-context.test.ts +290 -0
- package/src/server/generate-merge-context.ts +141 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-clone-policy.test.ts +138 -0
- package/src/server/git-clone-policy.ts +27 -0
- package/src/server/harness-types.ts +1 -0
- package/src/server/journey-verification.test.ts +640 -0
- package/src/server/journey-verification.ts +195 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/nats-auth.test.ts +92 -0
- package/src/server/nats-auth.ts +6 -0
- package/src/server/nats-bind-guard.test.ts +71 -0
- package/src/server/nats-bind-guard.ts +42 -0
- package/src/server/nats-bridge.test.ts +141 -0
- package/src/server/nats-bridge.ts +111 -0
- package/src/server/nats-connector.test.ts +56 -0
- package/src/server/nats-connector.ts +71 -0
- package/src/server/nats-daemon-manager.test.ts +76 -0
- package/src/server/nats-daemon-manager.ts +174 -0
- package/src/server/nats-publisher.test.ts +356 -0
- package/src/server/nats-publisher.ts +271 -0
- package/src/server/nats-responders.test.ts +1018 -0
- package/src/server/nats-responders.ts +925 -0
- package/src/server/nats-streams.test.ts +112 -0
- package/src/server/nats-streams.ts +129 -0
- package/src/server/oauth-pool/oauth-responders.ts +185 -0
- package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
- package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
- package/src/server/orchestration.test.ts +1063 -0
- package/src/server/orchestration.ts +716 -0
- package/src/server/pairing-endpoints.test.ts +171 -0
- package/src/server/pairing-store.test.ts +154 -0
- package/src/server/pairing-store.ts +124 -0
- package/src/server/paths.ts +27 -0
- package/src/server/pr3-liveness.test.ts +252 -0
- package/src/server/process-utils.ts +10 -0
- package/src/server/project-cli.ts +180 -0
- package/src/server/provider-catalog.test.ts +177 -0
- package/src/server/provider-catalog.ts +146 -0
- package/src/server/pty-responders.ts +345 -0
- package/src/server/push-notifications.test.ts +234 -0
- package/src/server/push-notifications.ts +188 -0
- package/src/server/quick-response.test.ts +196 -0
- package/src/server/quick-response.ts +154 -0
- package/src/server/read-models-agent-config.test.ts +59 -0
- package/src/server/read-models-coordination.test.ts +69 -0
- package/src/server/read-models.test.ts +332 -0
- package/src/server/read-models.ts +258 -0
- package/src/server/repo-journey.test.ts +96 -0
- package/src/server/repo-manager.test.ts +118 -0
- package/src/server/repo-manager.ts +97 -0
- package/src/server/repo-status.test.ts +54 -0
- package/src/server/repo-status.ts +82 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/runner-incompatible-gate.test.ts +383 -0
- package/src/server/runner-manager.test.ts +72 -0
- package/src/server/runner-manager.ts +312 -0
- package/src/server/runner-pairing-urls.test.ts +59 -0
- package/src/server/runner-pairing-urls.ts +67 -0
- package/src/server/runner-proxy.test.ts +456 -0
- package/src/server/runner-proxy.ts +494 -0
- package/src/server/runner-router.test.ts +429 -0
- package/src/server/runner-router.ts +212 -0
- package/src/server/runner-routing.test.ts +584 -0
- package/src/server/runtime-registry.test.ts +436 -0
- package/src/server/runtime-registry.ts +307 -0
- package/src/server/sandbox-health.test.ts +127 -0
- package/src/server/sandbox-health.ts +94 -0
- package/src/server/sandbox-journey.test.ts +232 -0
- package/src/server/sandbox-manager.test.ts +146 -0
- package/src/server/sandbox-manager.ts +159 -0
- package/src/server/server.test.ts +287 -0
- package/src/server/server.ts +1108 -0
- package/src/server/session-discovery.test.ts +129 -0
- package/src/server/session-discovery.ts +475 -0
- package/src/server/session-index.test.ts +362 -0
- package/src/server/session-index.ts +119 -0
- package/src/server/session-seed.ts +288 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/skill-discovery.test.ts +201 -0
- package/src/server/skill-discovery.ts +77 -0
- package/src/server/storage/test-helpers.ts +67 -0
- package/src/server/terminal-manager.test.ts +309 -0
- package/src/server/terminal-manager.ts +354 -0
- package/src/server/transcript-consumer.test.ts +339 -0
- package/src/server/transcript-consumer.ts +162 -0
- package/src/server/transcript-search.test.ts +193 -0
- package/src/server/transcript-search.ts +83 -0
- package/src/server/transcript-utils.ts +52 -0
- package/src/server/update-manager.test.ts +107 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/workflow-engine.test.ts +251 -0
- package/src/server/workflow-engine.ts +169 -0
- package/src/server/workflow-mcp.ts +49 -0
- package/src/server/workflow-store.test.ts +155 -0
- package/src/server/workflow-store.ts +139 -0
- package/src/server/workspace-agent-integration.test.ts +167 -0
- package/src/server/workspace-agent-routes.test.ts +127 -0
- package/src/server/workspace-agent-routes.ts +89 -0
- package/src/server/workspace-agent.test.ts +103 -0
- package/src/server/workspace-agent.ts +102 -0
- package/src/server/workspace-cli.test.ts +79 -0
- package/src/server/workspace-config-manager.test.ts +109 -0
- package/src/server/workspace-config-manager.ts +83 -0
- package/src/server/workspace-directory-policy.test.ts +109 -0
- package/src/server/workspace-directory-policy.ts +56 -0
- package/src/shared/agent-config-types.ts +25 -0
- package/src/shared/branding.test.ts +42 -0
- package/src/shared/branding.ts +54 -0
- package/src/shared/compression.test.ts +85 -0
- package/src/shared/compression.ts +42 -0
- package/src/shared/coordination-store.test.ts +24 -0
- package/src/shared/coordination-store.ts +26 -0
- package/src/shared/dev-ports.test.ts +84 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/extension-types.ts +45 -0
- package/src/shared/fork-presets.ts +54 -0
- package/src/shared/harness-types.test.ts +15 -0
- package/src/shared/harness-types.ts +21 -0
- package/src/shared/log-sink.test.ts +112 -0
- package/src/shared/log-sink.ts +185 -0
- package/src/shared/mention-pattern.ts +7 -0
- package/src/shared/merge-presets.ts +41 -0
- package/src/shared/nats-subjects.test.ts +61 -0
- package/src/shared/nats-subjects.ts +131 -0
- package/src/shared/permission-policy.ts +136 -0
- package/src/shared/ports.ts +3 -0
- package/src/shared/preset-types.ts +15 -0
- package/src/shared/profile-types.ts +49 -0
- package/src/shared/projectFileUrl.ts +36 -0
- package/src/shared/protocol.ts +153 -0
- package/src/shared/pty-instance.ts +43 -0
- package/src/shared/puggy/diagnostics/result.ts +18 -0
- package/src/shared/puggy/expressions/evaluate.ts +292 -0
- package/src/shared/puggy/index.test.ts +101 -0
- package/src/shared/puggy/index.ts +69 -0
- package/src/shared/puggy/parser/ast.ts +110 -0
- package/src/shared/puggy/parser/parser.ts +624 -0
- package/src/shared/puggy/renderer/html.ts +447 -0
- package/src/shared/runner-protocol.test.ts +277 -0
- package/src/shared/runner-protocol.ts +210 -0
- package/src/shared/runner-team-types.ts +73 -0
- package/src/shared/runtime-types.ts +48 -0
- package/src/shared/sandbox-types.ts +53 -0
- package/src/shared/tailwind-build.test.ts +12 -0
- package/src/shared/tinkaria-system-prompt.ts +101 -0
- package/src/shared/tools.test.ts +335 -0
- package/src/shared/tools.ts +397 -0
- package/src/shared/transcript-entries.ts +27 -0
- package/src/shared/transcript-render.test.ts +225 -0
- package/src/shared/transcript-render.ts +467 -0
- package/src/shared/types.ts +1031 -0
- package/src/shared/vite-config.test.ts +47 -0
- package/src/shared/web-context.test.ts +110 -0
- package/src/shared/web-context.ts +76 -0
- package/src/shared/workflow-types.ts +51 -0
- 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
|
+
})
|