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,195 @@
|
|
|
1
|
+
export type JourneyRouteExpectation =
|
|
2
|
+
| { kind: "exact"; value: string }
|
|
3
|
+
| { kind: "prefix"; value: string }
|
|
4
|
+
|
|
5
|
+
export interface JourneyStageSpec {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
owners: readonly string[]
|
|
9
|
+
route: JourneyRouteExpectation
|
|
10
|
+
requiredUiIds: readonly string[]
|
|
11
|
+
expectedText: readonly string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JourneySpec {
|
|
15
|
+
id: string
|
|
16
|
+
label: string
|
|
17
|
+
stages: readonly JourneyStageSpec[]
|
|
18
|
+
persistenceChecks: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StageProbeResult {
|
|
22
|
+
missing: string[]
|
|
23
|
+
c3ByUiId: Record<string, string | null>
|
|
24
|
+
textByUiId: Record<string, string | null>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type JourneyStageInventory = JourneyStageSpec
|
|
28
|
+
export type JourneyDefinition = JourneySpec
|
|
29
|
+
|
|
30
|
+
const HOME_READY_STAGE: JourneyStageSpec = {
|
|
31
|
+
id: "home.ready",
|
|
32
|
+
label: "Homepage connected with a visible project overview",
|
|
33
|
+
owners: ["c3-117"],
|
|
34
|
+
route: { kind: "exact", value: "/" },
|
|
35
|
+
requiredUiIds: [
|
|
36
|
+
"home.page",
|
|
37
|
+
"home.header",
|
|
38
|
+
"home.workspace-grid",
|
|
39
|
+
"home.project-card",
|
|
40
|
+
"home.project-overview",
|
|
41
|
+
"home.project-primary.action",
|
|
42
|
+
"home.project-secondary.action",
|
|
43
|
+
],
|
|
44
|
+
expectedText: [
|
|
45
|
+
"Workspaces",
|
|
46
|
+
"Project Overview",
|
|
47
|
+
"Start First Task",
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CHAT_READY_STAGE: JourneyStageSpec = {
|
|
52
|
+
id: "chat.ready",
|
|
53
|
+
label: "Chat shell rendered after starting a fresh task",
|
|
54
|
+
owners: ["c3-110", "c3-111", "c3-112"],
|
|
55
|
+
route: { kind: "prefix", value: "/chat/" },
|
|
56
|
+
requiredUiIds: [
|
|
57
|
+
"chat.page",
|
|
58
|
+
"chat.navbar",
|
|
59
|
+
"transcript.message-list",
|
|
60
|
+
"chat.composer",
|
|
61
|
+
],
|
|
62
|
+
expectedText: [],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const FORK_DIALOG_STAGE: JourneyStageSpec = {
|
|
66
|
+
id: "fork-dialog.open",
|
|
67
|
+
label: "Fork session dialog opened from the chat navbar",
|
|
68
|
+
owners: ["c3-110"],
|
|
69
|
+
route: { kind: "prefix", value: "/chat/" },
|
|
70
|
+
requiredUiIds: [
|
|
71
|
+
"chat.fork-session.dialog",
|
|
72
|
+
"chat.fork-session.context.input",
|
|
73
|
+
"chat.fork-session.submit.action",
|
|
74
|
+
"chat.fork-session.cancel.action",
|
|
75
|
+
"chat.fork-session.preset.action",
|
|
76
|
+
],
|
|
77
|
+
expectedText: [],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const MERGE_DIALOG_STAGE: JourneyStageSpec = {
|
|
81
|
+
id: "merge-dialog.open",
|
|
82
|
+
label: "Merge session dialog opened from the chat navbar",
|
|
83
|
+
owners: ["c3-110"],
|
|
84
|
+
route: { kind: "prefix", value: "/chat/" },
|
|
85
|
+
requiredUiIds: [
|
|
86
|
+
"chat.merge-session.dialog",
|
|
87
|
+
"chat.merge-session.sessions.list",
|
|
88
|
+
"chat.merge-session.context.input",
|
|
89
|
+
"chat.merge-session.submit.action",
|
|
90
|
+
"chat.merge-session.cancel.action",
|
|
91
|
+
],
|
|
92
|
+
expectedText: [],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const HOME_TO_NEW_CHAT_JOURNEY: JourneySpec = {
|
|
96
|
+
id: "homepage-to-new-chat",
|
|
97
|
+
label: "Homepage project overview -> Start First Task -> chat shell",
|
|
98
|
+
stages: [HOME_READY_STAGE, CHAT_READY_STAGE],
|
|
99
|
+
persistenceChecks: true,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const HOME_TO_FORK_DIALOG_JOURNEY: JourneySpec = {
|
|
103
|
+
id: "homepage-to-fork-dialog",
|
|
104
|
+
label: "Homepage project overview -> Start First Task -> fork dialog",
|
|
105
|
+
stages: [HOME_READY_STAGE, CHAT_READY_STAGE, FORK_DIALOG_STAGE],
|
|
106
|
+
persistenceChecks: true,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const HOME_TO_MERGE_DIALOG_JOURNEY: JourneySpec = {
|
|
110
|
+
id: "homepage-to-merge-dialog",
|
|
111
|
+
label: "Homepage project overview -> Start First Task -> merge dialog",
|
|
112
|
+
stages: [HOME_READY_STAGE, CHAT_READY_STAGE, MERGE_DIALOG_STAGE],
|
|
113
|
+
persistenceChecks: true,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function matchesJourneyRoute(pathname: string, expectation: JourneyRouteExpectation): boolean {
|
|
117
|
+
if (expectation.kind === "exact") {
|
|
118
|
+
return pathname === expectation.value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return pathname.startsWith(expectation.value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildStageProbeScript(stage: JourneyStageSpec): string {
|
|
125
|
+
const uiIds = JSON.stringify([...stage.requiredUiIds])
|
|
126
|
+
|
|
127
|
+
return `(() => {
|
|
128
|
+
const uiIds = ${uiIds};
|
|
129
|
+
const textByUiId = {};
|
|
130
|
+
const c3ByUiId = {};
|
|
131
|
+
const missing = [];
|
|
132
|
+
|
|
133
|
+
for (const uiId of uiIds) {
|
|
134
|
+
const selector = '[data-ui-id="' + uiId + '"]';
|
|
135
|
+
const element = document.querySelector(selector);
|
|
136
|
+
if (!element) {
|
|
137
|
+
missing.push(uiId);
|
|
138
|
+
c3ByUiId[uiId] = null;
|
|
139
|
+
textByUiId[uiId] = null;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
c3ByUiId[uiId] = element.getAttribute("data-ui-c3");
|
|
144
|
+
textByUiId[uiId] = element.textContent ? element.textContent.trim() : "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { missing, c3ByUiId, textByUiId };
|
|
148
|
+
})()`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getJourneyStage(id: JourneyStageSpec["id"]): JourneyStageSpec {
|
|
152
|
+
const stage = [
|
|
153
|
+
...HOME_TO_NEW_CHAT_JOURNEY.stages,
|
|
154
|
+
...HOME_TO_FORK_DIALOG_JOURNEY.stages,
|
|
155
|
+
...HOME_TO_MERGE_DIALOG_JOURNEY.stages,
|
|
156
|
+
].find((candidate) => candidate.id === id)
|
|
157
|
+
if (!stage) {
|
|
158
|
+
throw new Error(`Unknown journey stage: ${id}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return structuredClone(stage)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getJourneySpec(id: string): JourneySpec {
|
|
165
|
+
switch (id) {
|
|
166
|
+
case HOME_TO_NEW_CHAT_JOURNEY.id:
|
|
167
|
+
return structuredClone(HOME_TO_NEW_CHAT_JOURNEY)
|
|
168
|
+
case HOME_TO_FORK_DIALOG_JOURNEY.id:
|
|
169
|
+
return structuredClone(HOME_TO_FORK_DIALOG_JOURNEY)
|
|
170
|
+
case HOME_TO_MERGE_DIALOG_JOURNEY.id:
|
|
171
|
+
return structuredClone(HOME_TO_MERGE_DIALOG_JOURNEY)
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown journey: ${id}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getStageVerificationErrors(
|
|
178
|
+
observed: { url: string; uiIds: string[] },
|
|
179
|
+
stage: JourneyStageSpec,
|
|
180
|
+
): string[] {
|
|
181
|
+
const errors: string[] = []
|
|
182
|
+
const pathname = new URL(observed.url).pathname
|
|
183
|
+
|
|
184
|
+
if (!matchesJourneyRoute(pathname, stage.route)) {
|
|
185
|
+
errors.push(`expected route ${stage.route.kind} ${stage.route.value}, got ${pathname}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const uiId of stage.requiredUiIds) {
|
|
189
|
+
if (!observed.uiIds.includes(uiId)) {
|
|
190
|
+
errors.push(`missing ui id ${uiId}`)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return errors
|
|
195
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { hostname } from "node:os"
|
|
2
|
+
import process from "node:process"
|
|
3
|
+
import { spawnSync } from "node:child_process"
|
|
4
|
+
|
|
5
|
+
function runAndRead(command: string, args: string[]) {
|
|
6
|
+
const result = spawnSync(command, args, { encoding: "utf8" })
|
|
7
|
+
if (result.status !== 0) return null
|
|
8
|
+
const value = result.stdout.trim()
|
|
9
|
+
return value || null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getMachineDisplayName() {
|
|
13
|
+
if (process.platform === "darwin") {
|
|
14
|
+
const computerName = runAndRead("scutil", ["--get", "ComputerName"])
|
|
15
|
+
if (computerName) {
|
|
16
|
+
return computerName
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rawHostname = hostname().trim()
|
|
21
|
+
return rawHostname.replace(/\.local$|\.lan$/i, "") || "This Machine"
|
|
22
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, test, expect } from "bun:test"
|
|
2
|
+
import { NatsServer } from "@lagz0ne/nats-embedded"
|
|
3
|
+
import { connect, type NatsConnection } from "@nats-io/transport-node"
|
|
4
|
+
import { generateAuthToken } from "./nats-auth"
|
|
5
|
+
|
|
6
|
+
let server: NatsServer | null = null
|
|
7
|
+
let nc: NatsConnection | null = null
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
if (nc) {
|
|
11
|
+
await nc.drain()
|
|
12
|
+
nc = null
|
|
13
|
+
}
|
|
14
|
+
if (server) {
|
|
15
|
+
await server.stop()
|
|
16
|
+
server = null
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("generateAuthToken", () => {
|
|
21
|
+
test("returns a non-empty string", () => {
|
|
22
|
+
const token = generateAuthToken()
|
|
23
|
+
expect(typeof token).toBe("string")
|
|
24
|
+
expect(token.length).toBeGreaterThan(0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("produces unique tokens on each call", () => {
|
|
28
|
+
const tokens = new Set(Array.from({ length: 100 }, () => generateAuthToken()))
|
|
29
|
+
expect(tokens.size).toBe(100)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("token has sufficient entropy (>= 32 chars)", () => {
|
|
33
|
+
const token = generateAuthToken()
|
|
34
|
+
expect(token.length).toBeGreaterThanOrEqual(32)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("NATS token auth integration", () => {
|
|
39
|
+
test("authenticated client connects successfully", async () => {
|
|
40
|
+
const token = generateAuthToken()
|
|
41
|
+
server = await NatsServer.start({ token })
|
|
42
|
+
nc = await connect({ servers: server.url, token })
|
|
43
|
+
|
|
44
|
+
// Verify connection works by publishing + subscribing
|
|
45
|
+
const received: string[] = []
|
|
46
|
+
const sub = nc.subscribe("test.auth")
|
|
47
|
+
void (async () => {
|
|
48
|
+
for await (const msg of sub) {
|
|
49
|
+
received.push(new TextDecoder().decode(msg.data))
|
|
50
|
+
sub.unsubscribe()
|
|
51
|
+
}
|
|
52
|
+
})()
|
|
53
|
+
|
|
54
|
+
nc.publish("test.auth", new TextEncoder().encode("hello"))
|
|
55
|
+
await nc.flush()
|
|
56
|
+
// Give subscription a moment to process
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
58
|
+
expect(received).toContain("hello")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("unauthenticated client is rejected", async () => {
|
|
62
|
+
const token = generateAuthToken()
|
|
63
|
+
server = await NatsServer.start({ token })
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
nc = await connect({ servers: server.url })
|
|
67
|
+
// If connection somehow succeeds, verify it can't operate
|
|
68
|
+
await nc.flush()
|
|
69
|
+
// Should not reach here
|
|
70
|
+
expect(true).toBe(false)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
73
|
+
expect(message.toLowerCase()).toMatch(/authorization|auth|denied|closed/)
|
|
74
|
+
nc = null
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("client with wrong token is rejected", async () => {
|
|
79
|
+
const token = generateAuthToken()
|
|
80
|
+
server = await NatsServer.start({ token })
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
nc = await connect({ servers: server.url, token: "wrong-token" })
|
|
84
|
+
await nc.flush()
|
|
85
|
+
expect(true).toBe(false)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
88
|
+
expect(message.toLowerCase()).toMatch(/authorization|auth|denied|closed/)
|
|
89
|
+
nc = null
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
import { requiresCalloutForBind } from "./nats-bind-guard"
|
|
3
|
+
|
|
4
|
+
describe("requiresCalloutForBind", () => {
|
|
5
|
+
// ── Allowed combinations ────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
test("loopback 127.0.0.1 + token → ok (dev default)", () => {
|
|
8
|
+
expect(requiresCalloutForBind("127.0.0.1", "token")).toEqual({ ok: true })
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("loopback localhost + token → ok", () => {
|
|
12
|
+
expect(requiresCalloutForBind("localhost", "token")).toEqual({ ok: true })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("loopback ::1 + token → ok", () => {
|
|
16
|
+
expect(requiresCalloutForBind("::1", "token")).toEqual({ ok: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("loopback 127.0.0.1 + callout → ok", () => {
|
|
20
|
+
expect(requiresCalloutForBind("127.0.0.1", "callout")).toEqual({ ok: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("wide 0.0.0.0 + callout → ok (tailnet path)", () => {
|
|
24
|
+
expect(requiresCalloutForBind("0.0.0.0", "callout")).toEqual({ ok: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("tailnet IP + callout → ok", () => {
|
|
28
|
+
expect(requiresCalloutForBind("100.64.1.1", "callout")).toEqual({ ok: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// ── Disallowed combinations ─────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
test("wide 0.0.0.0 + token → refused with reason", () => {
|
|
34
|
+
const result = requiresCalloutForBind("0.0.0.0", "token")
|
|
35
|
+
expect(result.ok).toBe(false)
|
|
36
|
+
expect(result.reason).toMatch(/Refusing to bind NATS to 0\.0\.0\.0 in token mode/)
|
|
37
|
+
expect(result.reason).toMatch(/NATS_AUTH_MODE=callout/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("tailnet IP + token → refused with reason", () => {
|
|
41
|
+
const result = requiresCalloutForBind("100.64.1.1", "token")
|
|
42
|
+
expect(result.ok).toBe(false)
|
|
43
|
+
expect(result.reason).toMatch(/Refusing to bind NATS to 100\.64\.1\.1 in token mode/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("arbitrary private IP + token → refused", () => {
|
|
47
|
+
const result = requiresCalloutForBind("192.168.1.10", "token")
|
|
48
|
+
expect(result.ok).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("reason message is non-empty when refused", () => {
|
|
52
|
+
const result = requiresCalloutForBind("10.0.0.1", "token")
|
|
53
|
+
expect(result.ok).toBe(false)
|
|
54
|
+
expect(typeof result.reason).toBe("string")
|
|
55
|
+
expect((result.reason ?? "").length).toBeGreaterThan(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ── Case-insensitive loopback match ─────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test("LOCALHOST (uppercase) + token → ok (case-insensitive loopback)", () => {
|
|
61
|
+
expect(requiresCalloutForBind("LOCALHOST", "token")).toEqual({ ok: true })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("Localhost (mixed-case) + token → ok", () => {
|
|
65
|
+
expect(requiresCalloutForBind("Localhost", "token")).toEqual({ ok: true })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test(" localhost (whitespace) + token → ok", () => {
|
|
69
|
+
expect(requiresCalloutForBind(" localhost ", "token")).toEqual({ ok: true })
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard: a wide (non-loopback) NATS bind is only permitted in callout mode.
|
|
3
|
+
*
|
|
4
|
+
* Decision 0007: never expose a shared-token bus on the tailnet.
|
|
5
|
+
* Loopback + token stays allowed (dev default).
|
|
6
|
+
* Non-loopback + callout is allowed (tailnet path); logs a note.
|
|
7
|
+
* Non-loopback + token is refused at startup with a fatal error message.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The set of host values that are considered loopback/local-only. */
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"])
|
|
12
|
+
|
|
13
|
+
export type AuthMode = "callout" | "token"
|
|
14
|
+
|
|
15
|
+
export interface BindGuardResult {
|
|
16
|
+
ok: boolean
|
|
17
|
+
/** Present only when ok is false. */
|
|
18
|
+
reason?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pure predicate — no side effects.
|
|
23
|
+
*
|
|
24
|
+
* Returns `{ ok: false, reason }` when binding a non-loopback host in token
|
|
25
|
+
* mode (decision 0007), `{ ok: true }` otherwise.
|
|
26
|
+
*/
|
|
27
|
+
export function requiresCalloutForBind(
|
|
28
|
+
host: string,
|
|
29
|
+
authMode: AuthMode,
|
|
30
|
+
): BindGuardResult {
|
|
31
|
+
const isLoopback = LOOPBACK_HOSTS.has(host.trim().toLowerCase())
|
|
32
|
+
if (!isLoopback && authMode !== "callout") {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason:
|
|
36
|
+
`Refusing to bind NATS to ${host} in token mode — ` +
|
|
37
|
+
`a shared-token bus must not be exposed beyond loopback; ` +
|
|
38
|
+
`set NATS_AUTH_MODE=callout`,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { ok: true }
|
|
42
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { afterEach, describe, test, expect } from "bun:test"
|
|
2
|
+
import { NatsBridge } from "./nats-bridge"
|
|
3
|
+
import { connect } from "@nats-io/transport-node"
|
|
4
|
+
|
|
5
|
+
const decoder = new TextDecoder()
|
|
6
|
+
let bridge: NatsBridge | null = null
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
if (bridge) {
|
|
10
|
+
await bridge.dispose()
|
|
11
|
+
bridge = null
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe("NatsBridge", () => {
|
|
16
|
+
test("create() starts NATS server and connects", async () => {
|
|
17
|
+
bridge = await NatsBridge.create()
|
|
18
|
+
expect(bridge.natsUrl).toMatch(/^nats:\/\/127\.0\.0\.1:\d+$/)
|
|
19
|
+
expect(bridge.natsWsUrl).toMatch(/^ws:\/\/127\.0\.0\.1:\d+\/?$/)
|
|
20
|
+
expect(bridge.natsWsPort).toBeGreaterThan(0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("create() can bind on all interfaces while advertising localhost URLs", async () => {
|
|
24
|
+
bridge = await NatsBridge.create({
|
|
25
|
+
bindHost: "0.0.0.0",
|
|
26
|
+
advertisedHost: "127.0.0.1",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(bridge.natsUrl).toMatch(/^nats:\/\/127\.0\.0\.1:\d+$/)
|
|
30
|
+
expect(bridge.natsWsUrl).toMatch(/^ws:\/\/127\.0\.0\.1:\d+\/?$/)
|
|
31
|
+
|
|
32
|
+
const testClient = await connect({ servers: bridge.natsUrl })
|
|
33
|
+
await testClient.drain()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("publish() delivers message to subscriber", async () => {
|
|
37
|
+
bridge = await NatsBridge.create()
|
|
38
|
+
|
|
39
|
+
// Connect a test subscriber
|
|
40
|
+
const testClient = await connect({ servers: bridge.natsUrl })
|
|
41
|
+
const sub = testClient.subscribe("test.subject")
|
|
42
|
+
await testClient.flush()
|
|
43
|
+
|
|
44
|
+
// Publish via bridge
|
|
45
|
+
bridge.publish("test.subject", { hello: "world" })
|
|
46
|
+
|
|
47
|
+
// Receive message
|
|
48
|
+
const msg = await (async () => {
|
|
49
|
+
for await (const m of sub) {
|
|
50
|
+
return JSON.parse(decoder.decode(m.data))
|
|
51
|
+
}
|
|
52
|
+
})()
|
|
53
|
+
|
|
54
|
+
expect(msg).toEqual({ hello: "world" })
|
|
55
|
+
|
|
56
|
+
await testClient.drain()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("publish() handles multiple subjects", async () => {
|
|
60
|
+
bridge = await NatsBridge.create()
|
|
61
|
+
|
|
62
|
+
const testClient = await connect({ servers: bridge.natsUrl })
|
|
63
|
+
const received: Array<{ subject: string; data: unknown }> = []
|
|
64
|
+
|
|
65
|
+
const sub1 = testClient.subscribe("runtime.snap.sidebar")
|
|
66
|
+
const sub2 = testClient.subscribe("runtime.snap.chat.abc")
|
|
67
|
+
|
|
68
|
+
// Collect in background
|
|
69
|
+
const collect = async (sub: AsyncIterable<{ data: Uint8Array; subject: string }>, count: number) => {
|
|
70
|
+
let n = 0
|
|
71
|
+
for await (const m of sub) {
|
|
72
|
+
received.push({ subject: m.subject, data: JSON.parse(decoder.decode(m.data)) })
|
|
73
|
+
if (++n >= count) break
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const p1 = collect(sub1, 1)
|
|
78
|
+
const p2 = collect(sub2, 1)
|
|
79
|
+
|
|
80
|
+
await testClient.flush()
|
|
81
|
+
|
|
82
|
+
bridge.publish("runtime.snap.sidebar", { type: "sidebar" })
|
|
83
|
+
bridge.publish("runtime.snap.chat.abc", { type: "chat", chatId: "abc" })
|
|
84
|
+
|
|
85
|
+
await Promise.all([p1, p2])
|
|
86
|
+
|
|
87
|
+
expect(received).toHaveLength(2)
|
|
88
|
+
expect(received.find((r) => r.subject === "runtime.snap.sidebar")?.data).toEqual({ type: "sidebar" })
|
|
89
|
+
expect(received.find((r) => r.subject === "runtime.snap.chat.abc")?.data).toEqual({ type: "chat", chatId: "abc" })
|
|
90
|
+
|
|
91
|
+
await testClient.drain()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("dispose() cleanly stops NATS server", async () => {
|
|
95
|
+
bridge = await NatsBridge.create()
|
|
96
|
+
const url = bridge.natsUrl
|
|
97
|
+
|
|
98
|
+
await bridge.dispose()
|
|
99
|
+
bridge = null
|
|
100
|
+
|
|
101
|
+
// Connection to stopped server should fail
|
|
102
|
+
try {
|
|
103
|
+
await connect({ servers: url, maxReconnectAttempts: 0, reconnect: false })
|
|
104
|
+
expect(true).toBe(false) // should not reach
|
|
105
|
+
} catch (error) {
|
|
106
|
+
expect(error).toBeDefined()
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("wildcard subscription receives matching messages", async () => {
|
|
111
|
+
bridge = await NatsBridge.create()
|
|
112
|
+
|
|
113
|
+
const testClient = await connect({ servers: bridge.natsUrl })
|
|
114
|
+
const received: string[] = []
|
|
115
|
+
|
|
116
|
+
const sub = testClient.subscribe("runtime.snap.>")
|
|
117
|
+
|
|
118
|
+
const collect = async (count: number) => {
|
|
119
|
+
let n = 0
|
|
120
|
+
for await (const m of sub) {
|
|
121
|
+
received.push(m.subject)
|
|
122
|
+
if (++n >= count) break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const p = collect(3)
|
|
127
|
+
await testClient.flush()
|
|
128
|
+
|
|
129
|
+
bridge.publish("runtime.snap.sidebar", {})
|
|
130
|
+
bridge.publish("runtime.snap.update", {})
|
|
131
|
+
bridge.publish("runtime.snap.chat.xyz", {})
|
|
132
|
+
|
|
133
|
+
await p
|
|
134
|
+
|
|
135
|
+
expect(received).toContain("runtime.snap.sidebar")
|
|
136
|
+
expect(received).toContain("runtime.snap.update")
|
|
137
|
+
expect(received).toContain("runtime.snap.chat.xyz")
|
|
138
|
+
|
|
139
|
+
await testClient.drain()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { NatsServer } from "@lagz0ne/nats-embedded"
|
|
2
|
+
import { connect, type NatsConnection } from "@nats-io/transport-node"
|
|
3
|
+
import { LOG_PREFIX } from "../shared/branding"
|
|
4
|
+
|
|
5
|
+
const encoder = new TextEncoder()
|
|
6
|
+
|
|
7
|
+
function tokenOption(token?: string): { token: string } | undefined {
|
|
8
|
+
return token ? { token } : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CreateNatsBridgeOptions {
|
|
12
|
+
token?: string
|
|
13
|
+
bindHost?: string
|
|
14
|
+
advertisedHost?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeAdvertisedHost(bindHost: string, advertisedHost?: string): string {
|
|
18
|
+
if (advertisedHost) return advertisedHost
|
|
19
|
+
return bindHost === "0.0.0.0" ? "127.0.0.1" : bindHost
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rewriteUrlHost(rawUrl: string, host: string): string {
|
|
23
|
+
const url = new URL(rawUrl)
|
|
24
|
+
url.hostname = host
|
|
25
|
+
return url.toString().replace(/\/$/, "")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class NatsBridge {
|
|
29
|
+
readonly natsUrl: string
|
|
30
|
+
readonly natsWsUrl: string
|
|
31
|
+
readonly natsWsPort: number
|
|
32
|
+
readonly authToken: string | undefined
|
|
33
|
+
|
|
34
|
+
private readonly server: NatsServer
|
|
35
|
+
readonly nc: NatsConnection
|
|
36
|
+
private disposed = false
|
|
37
|
+
|
|
38
|
+
private constructor(
|
|
39
|
+
server: NatsServer,
|
|
40
|
+
connection: NatsConnection,
|
|
41
|
+
advertisedNatsUrl: string,
|
|
42
|
+
advertisedWsUrl: string,
|
|
43
|
+
token?: string
|
|
44
|
+
) {
|
|
45
|
+
this.server = server
|
|
46
|
+
this.nc = connection
|
|
47
|
+
this.natsUrl = advertisedNatsUrl
|
|
48
|
+
this.natsWsUrl = advertisedWsUrl
|
|
49
|
+
this.natsWsPort = server.wsPort!
|
|
50
|
+
this.authToken = token
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async create(options: CreateNatsBridgeOptions = {}): Promise<NatsBridge> {
|
|
54
|
+
const bindHost = options.bindHost ?? "127.0.0.1"
|
|
55
|
+
const advertisedHost = normalizeAdvertisedHost(bindHost, options.advertisedHost)
|
|
56
|
+
const server = await NatsServer.start({
|
|
57
|
+
host: bindHost,
|
|
58
|
+
websocket: true,
|
|
59
|
+
jetstream: true,
|
|
60
|
+
...tokenOption(options.token),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!server.wsUrl || !server.wsPort) {
|
|
64
|
+
await server.stop()
|
|
65
|
+
throw new Error("NATS server started without WebSocket support")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const connection = await connect({
|
|
69
|
+
servers: rewriteUrlHost(server.url, advertisedHost),
|
|
70
|
+
...tokenOption(options.token),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const advertisedNatsUrl = rewriteUrlHost(server.url, advertisedHost)
|
|
74
|
+
const advertisedWsUrl = rewriteUrlHost(server.wsUrl, advertisedHost)
|
|
75
|
+
|
|
76
|
+
console.warn(LOG_PREFIX, `NATS bridge started — TCP: ${advertisedNatsUrl}, WS: ${advertisedWsUrl}`)
|
|
77
|
+
|
|
78
|
+
const bridge = new NatsBridge(server, connection, advertisedNatsUrl, advertisedWsUrl, options.token)
|
|
79
|
+
|
|
80
|
+
void server.exited.then((code) => {
|
|
81
|
+
if (!bridge.disposed) {
|
|
82
|
+
console.warn(LOG_PREFIX, `NATS server exited unexpectedly with code ${code}`)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return bridge
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
publish(subject: string, data: unknown): void {
|
|
90
|
+
if (this.disposed) return
|
|
91
|
+
try {
|
|
92
|
+
this.nc.publish(subject, encoder.encode(JSON.stringify(data)))
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
95
|
+
console.warn(LOG_PREFIX, `NATS publish failed on ${subject}: ${message}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async dispose(): Promise<void> {
|
|
100
|
+
if (this.disposed) return
|
|
101
|
+
this.disposed = true
|
|
102
|
+
try {
|
|
103
|
+
await this.nc.drain()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
106
|
+
console.warn(LOG_PREFIX, `NATS connection drain failed: ${message}`)
|
|
107
|
+
}
|
|
108
|
+
await this.server.stop()
|
|
109
|
+
console.warn(LOG_PREFIX, "NATS bridge stopped")
|
|
110
|
+
}
|
|
111
|
+
}
|