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