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,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the pairing endpoints (PR2 Stage 1).
|
|
3
|
+
*
|
|
4
|
+
* Starts a real embedded NATS server on a dedicated port and exercises
|
|
5
|
+
* POST /api/pairing/code and POST /api/pairing/exchange over HTTP.
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - code → exchange round-trip; returned token verifies via verifyCredentialToken.
|
|
9
|
+
* - second exchange of same code → 410 (consumed).
|
|
10
|
+
* - unknown code → 400.
|
|
11
|
+
* - token mode (no callout tokenSecret) → 409 on code issue.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterEach, describe, test, expect } from "bun:test"
|
|
15
|
+
import { startServer } from "./server"
|
|
16
|
+
import { verifyCredentialToken } from "../nats/auth-callout/token"
|
|
17
|
+
import { ensureCalloutKeys } from "../nats/auth-callout/keys"
|
|
18
|
+
import path from "node:path"
|
|
19
|
+
import os from "node:os"
|
|
20
|
+
import fs from "node:fs"
|
|
21
|
+
|
|
22
|
+
type StartedServer = Awaited<ReturnType<typeof startServer>>
|
|
23
|
+
|
|
24
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
async function post(port: number, pathname: string, body?: unknown): Promise<Response> {
|
|
27
|
+
return fetch(`http://127.0.0.1:${port}${pathname}`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: body !== undefined ? { "Content-Type": "application/json" } : {},
|
|
30
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Callout-mode suite ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe("pairing endpoints (callout mode)", () => {
|
|
37
|
+
let started: StartedServer | null = null
|
|
38
|
+
let natsDataDir: string
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
await started?.stop()
|
|
42
|
+
started = null
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("code → exchange round-trip; token verifies as runner credential", async () => {
|
|
46
|
+
natsDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pr2-pair-test-"))
|
|
47
|
+
process.env.NATS_AUTH_MODE = "callout"
|
|
48
|
+
process.env.NATS_DATA_DIR = natsDataDir
|
|
49
|
+
started = await startServer({ port: 4381, host: "127.0.0.1", strictPort: true })
|
|
50
|
+
const port = started.port
|
|
51
|
+
|
|
52
|
+
// Issue a pairing code.
|
|
53
|
+
const issueRes = await post(port, "/api/pairing/code")
|
|
54
|
+
expect(issueRes.status).toBe(200)
|
|
55
|
+
const issued = await issueRes.json() as { code: string; expiresAt: number }
|
|
56
|
+
expect(typeof issued.code).toBe("string")
|
|
57
|
+
expect(issued.code).toMatch(/^[a-z2-7]{5}-[a-z2-7]{5}-[a-z2-7]{6}$/)
|
|
58
|
+
expect(typeof issued.expiresAt).toBe("number")
|
|
59
|
+
expect(issued.expiresAt).toBeGreaterThan(Date.now())
|
|
60
|
+
|
|
61
|
+
// Exchange the code.
|
|
62
|
+
const exchangeRes = await post(port, "/api/pairing/exchange", { code: issued.code })
|
|
63
|
+
expect(exchangeRes.status).toBe(200)
|
|
64
|
+
const exchanged = await exchangeRes.json() as {
|
|
65
|
+
runnerId: string; token: string; natsUrl: string; natsWsUrl: string
|
|
66
|
+
}
|
|
67
|
+
expect(typeof exchanged.runnerId).toBe("string")
|
|
68
|
+
expect(exchanged.runnerId).toMatch(/^runner-/)
|
|
69
|
+
expect(typeof exchanged.token).toBe("string")
|
|
70
|
+
expect(typeof exchanged.natsUrl).toBe("string")
|
|
71
|
+
expect(typeof exchanged.natsWsUrl).toBe("string")
|
|
72
|
+
|
|
73
|
+
// Verify the returned token decodes to the correct runner identity.
|
|
74
|
+
const keys = await ensureCalloutKeys(natsDataDir)
|
|
75
|
+
const identity = await verifyCredentialToken(exchanged.token, keys.tokenSecret)
|
|
76
|
+
expect(identity).toEqual({ class: "runner", runnerId: exchanged.runnerId })
|
|
77
|
+
}, 30_000)
|
|
78
|
+
|
|
79
|
+
test("second exchange of same code returns 410 (consumed)", async () => {
|
|
80
|
+
natsDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pr2-pair-test-"))
|
|
81
|
+
process.env.NATS_AUTH_MODE = "callout"
|
|
82
|
+
process.env.NATS_DATA_DIR = natsDataDir
|
|
83
|
+
started = await startServer({ port: 4382, host: "127.0.0.1", strictPort: true })
|
|
84
|
+
const port = started.port
|
|
85
|
+
|
|
86
|
+
const { code } = await post(port, "/api/pairing/code").then((r) => r.json()) as { code: string }
|
|
87
|
+
// First exchange — succeeds.
|
|
88
|
+
const first = await post(port, "/api/pairing/exchange", { code })
|
|
89
|
+
expect(first.status).toBe(200)
|
|
90
|
+
// Second exchange — consumed.
|
|
91
|
+
const second = await post(port, "/api/pairing/exchange", { code })
|
|
92
|
+
expect(second.status).toBe(410)
|
|
93
|
+
const body = await second.json() as { error: string }
|
|
94
|
+
expect(body.error).toBe("consumed")
|
|
95
|
+
}, 30_000)
|
|
96
|
+
|
|
97
|
+
test("unknown code returns 400", async () => {
|
|
98
|
+
natsDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pr2-pair-test-"))
|
|
99
|
+
process.env.NATS_AUTH_MODE = "callout"
|
|
100
|
+
process.env.NATS_DATA_DIR = natsDataDir
|
|
101
|
+
started = await startServer({ port: 4383, host: "127.0.0.1", strictPort: true })
|
|
102
|
+
const port = started.port
|
|
103
|
+
|
|
104
|
+
const res = await post(port, "/api/pairing/exchange", { code: "aaaaa-bbbbb-cccccc" })
|
|
105
|
+
expect(res.status).toBe(400)
|
|
106
|
+
const body = await res.json() as { error: string }
|
|
107
|
+
expect(body.error).toBe("unknown")
|
|
108
|
+
}, 30_000)
|
|
109
|
+
|
|
110
|
+
test("bound to 0.0.0.0 without NATS_ADVERTISED_HOST → exchange rejects with 409", async () => {
|
|
111
|
+
natsDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pr2-pair-test-"))
|
|
112
|
+
process.env.NATS_AUTH_MODE = "callout"
|
|
113
|
+
process.env.NATS_DATA_DIR = natsDataDir
|
|
114
|
+
delete process.env.NATS_ADVERTISED_HOST
|
|
115
|
+
// Bind to 0.0.0.0 — the daemon URL becomes nats://0.0.0.0:<port>, which a
|
|
116
|
+
// remote runner cannot reach. The exchange must refuse, not hand it out.
|
|
117
|
+
started = await startServer({ port: 4385, host: "0.0.0.0", strictPort: true })
|
|
118
|
+
const port = started.port
|
|
119
|
+
|
|
120
|
+
const { code } = await post(port, "/api/pairing/code").then((r) => r.json()) as { code: string }
|
|
121
|
+
const res = await post(port, "/api/pairing/exchange", { code })
|
|
122
|
+
expect(res.status).toBe(409)
|
|
123
|
+
const body = await res.json() as { error: string }
|
|
124
|
+
expect(body.error).toContain("NATS_ADVERTISED_HOST")
|
|
125
|
+
}, 30_000)
|
|
126
|
+
|
|
127
|
+
test("bound to 0.0.0.0 with NATS_ADVERTISED_HOST → natsUrl carries the advertised host", async () => {
|
|
128
|
+
natsDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pr2-pair-test-"))
|
|
129
|
+
process.env.NATS_AUTH_MODE = "callout"
|
|
130
|
+
process.env.NATS_DATA_DIR = natsDataDir
|
|
131
|
+
process.env.NATS_ADVERTISED_HOST = "100.64.0.2"
|
|
132
|
+
try {
|
|
133
|
+
started = await startServer({ port: 4386, host: "0.0.0.0", strictPort: true })
|
|
134
|
+
const port = started.port
|
|
135
|
+
|
|
136
|
+
const { code } = await post(port, "/api/pairing/code").then((r) => r.json()) as { code: string }
|
|
137
|
+
const res = await post(port, "/api/pairing/exchange", { code })
|
|
138
|
+
expect(res.status).toBe(200)
|
|
139
|
+
const body = await res.json() as { natsUrl: string; natsWsUrl: string }
|
|
140
|
+
expect(new URL(body.natsUrl).hostname).toBe("100.64.0.2")
|
|
141
|
+
expect(new URL(body.natsWsUrl).hostname).toBe("100.64.0.2")
|
|
142
|
+
} finally {
|
|
143
|
+
delete process.env.NATS_ADVERTISED_HOST
|
|
144
|
+
}
|
|
145
|
+
}, 30_000)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ── Token-mode suite ──────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("pairing endpoints (token mode)", () => {
|
|
151
|
+
let started: StartedServer | null = null
|
|
152
|
+
|
|
153
|
+
afterEach(async () => {
|
|
154
|
+
await started?.stop()
|
|
155
|
+
started = null
|
|
156
|
+
delete process.env.NATS_AUTH_MODE
|
|
157
|
+
delete process.env.NATS_DATA_DIR
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("POST /api/pairing/code returns 409 in token mode", async () => {
|
|
161
|
+
process.env.NATS_AUTH_MODE = "token"
|
|
162
|
+
delete process.env.NATS_DATA_DIR
|
|
163
|
+
started = await startServer({ port: 4384, host: "127.0.0.1", strictPort: true })
|
|
164
|
+
const port = started.port
|
|
165
|
+
|
|
166
|
+
const res = await post(port, "/api/pairing/code")
|
|
167
|
+
expect(res.status).toBe(409)
|
|
168
|
+
const body = await res.json() as { error: string }
|
|
169
|
+
expect(body.error).toContain("callout")
|
|
170
|
+
}, 30_000)
|
|
171
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for PairingStore (PR2 Stage 1).
|
|
3
|
+
*
|
|
4
|
+
* Uses injectable clock and RNG so tests are deterministic and fast.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect } from "bun:test"
|
|
8
|
+
import { PairingStore } from "./pairing-store"
|
|
9
|
+
|
|
10
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
function makeEntry() {
|
|
13
|
+
return { runnerId: "runner-test-1", token: "tok.sig" }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A store with a controllable clock (ms) and a fixed-byte RNG. */
|
|
17
|
+
function makeStore(startMs = 1_000_000, rngByte = 0xab) {
|
|
18
|
+
let now = startMs
|
|
19
|
+
const tick = (ms: number) => { now += ms }
|
|
20
|
+
const store = new PairingStore({
|
|
21
|
+
now: () => now,
|
|
22
|
+
randomBytes: (n) => new Uint8Array(n).fill(rngByte),
|
|
23
|
+
ttlMs: 60_000, // 1 min for tests
|
|
24
|
+
})
|
|
25
|
+
return { store, tick }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A store with a controllable clock and a counter-based RNG (unique codes per issue). */
|
|
29
|
+
function makeCounterStore(startMs = 1_000_000) {
|
|
30
|
+
let now = startMs
|
|
31
|
+
let counter = 0
|
|
32
|
+
const tick = (ms: number) => { now += ms }
|
|
33
|
+
const store = new PairingStore({
|
|
34
|
+
now: () => now,
|
|
35
|
+
randomBytes: (n) => {
|
|
36
|
+
const buf = new Uint8Array(n)
|
|
37
|
+
// Fill with an incrementing byte so each call produces a distinct value.
|
|
38
|
+
buf.fill(counter++ & 0xff)
|
|
39
|
+
return buf
|
|
40
|
+
},
|
|
41
|
+
ttlMs: 60_000,
|
|
42
|
+
})
|
|
43
|
+
return { store, tick }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Code shape ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("PairingStore.issue", () => {
|
|
49
|
+
test("returns a code and expiresAt", () => {
|
|
50
|
+
const startMs = 1_000_000
|
|
51
|
+
const { store } = makeStore(startMs)
|
|
52
|
+
const { code, expiresAt } = store.issue(makeEntry())
|
|
53
|
+
expect(typeof code).toBe("string")
|
|
54
|
+
expect(code.length).toBeGreaterThan(0)
|
|
55
|
+
// expiresAt should be in the future relative to our injected clock start
|
|
56
|
+
expect(expiresAt).toBeGreaterThan(startMs)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("code has dashed base32 shape (xxxxx-xxxxx-xxxxxx)", () => {
|
|
60
|
+
const { store } = makeStore()
|
|
61
|
+
const { code } = store.issue(makeEntry())
|
|
62
|
+
// 5-5-6 groups separated by dashes (16 base32 chars), lowercase a-z and 2-7
|
|
63
|
+
expect(code).toMatch(/^[a-z2-7]{5}-[a-z2-7]{5}-[a-z2-7]{6}$/)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("codes from different RNG bytes are unique", () => {
|
|
67
|
+
// Two stores with different RNG seeds produce different codes.
|
|
68
|
+
const { store: s1 } = makeStore(1_000_000, 0x11)
|
|
69
|
+
const { store: s2 } = makeStore(1_000_000, 0x22)
|
|
70
|
+
const { code: c1 } = s1.issue(makeEntry())
|
|
71
|
+
const { code: c2 } = s2.issue(makeEntry())
|
|
72
|
+
expect(c1).not.toBe(c2)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("expiresAt equals now + ttlMs", () => {
|
|
76
|
+
const startMs = 5_000_000
|
|
77
|
+
const { store } = makeStore(startMs)
|
|
78
|
+
const { expiresAt } = store.issue(makeEntry())
|
|
79
|
+
expect(expiresAt).toBe(startMs + 60_000)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// ── Exchange — happy path ─────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe("PairingStore.exchange", () => {
|
|
86
|
+
test("exchange returns runnerId and token on first use", () => {
|
|
87
|
+
const { store } = makeStore()
|
|
88
|
+
const entry = makeEntry()
|
|
89
|
+
const { code } = store.issue(entry)
|
|
90
|
+
const result = store.exchange(code)
|
|
91
|
+
expect(result).toEqual({ ok: true, runnerId: entry.runnerId, token: entry.token })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("second exchange of same code returns consumed", () => {
|
|
95
|
+
const { store } = makeStore()
|
|
96
|
+
const { code } = store.issue(makeEntry())
|
|
97
|
+
store.exchange(code) // first — succeeds
|
|
98
|
+
const result = store.exchange(code) // second
|
|
99
|
+
expect(result).toEqual({ ok: false, error: "consumed" })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── Expiry ────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test("expired code (clock advanced past expiresAt) returns expired", () => {
|
|
105
|
+
const { store, tick } = makeStore()
|
|
106
|
+
const { code } = store.issue(makeEntry())
|
|
107
|
+
tick(60_001) // just past the 1-min TTL
|
|
108
|
+
const result = store.exchange(code)
|
|
109
|
+
expect(result).toEqual({ ok: false, error: "expired" })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("code exactly at expiresAt boundary is expired (>= check)", () => {
|
|
113
|
+
const { store, tick } = makeStore()
|
|
114
|
+
const { code, expiresAt } = store.issue(makeEntry())
|
|
115
|
+
// Advance so now === expiresAt exactly.
|
|
116
|
+
tick(expiresAt - 1_000_000) // start was 1_000_000
|
|
117
|
+
const result = store.exchange(code)
|
|
118
|
+
expect(result).toEqual({ ok: false, error: "expired" })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("unknown code returns unknown", () => {
|
|
122
|
+
const { store } = makeStore()
|
|
123
|
+
const result = store.exchange("xxxxx-yyyyy-zzzz")
|
|
124
|
+
expect(result).toEqual({ ok: false, error: "unknown" })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// ── Sweep ─────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
test("expired entries are swept from internal map on next issue", () => {
|
|
130
|
+
// After an issue() triggers a sweep, previously-expired entries are deleted.
|
|
131
|
+
// A subsequent exchange() of the old code returns "unknown" (entry gone).
|
|
132
|
+
const { store, tick } = makeCounterStore()
|
|
133
|
+
const { code } = store.issue(makeEntry())
|
|
134
|
+
tick(60_001) // expire it
|
|
135
|
+
// Second issue produces a different code (counter RNG) and triggers sweep.
|
|
136
|
+
store.issue({ runnerId: "runner-2", token: "tok2.sig" })
|
|
137
|
+
// First entry was swept — exchange now reports "unknown".
|
|
138
|
+
const result = store.exchange(code)
|
|
139
|
+
expect(result).toEqual({ ok: false, error: "unknown" })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("consumed entries are also evicted after TTL (no permanent consumed-oracle / memory leak)", () => {
|
|
143
|
+
// Within the TTL window a reused code reports "consumed"...
|
|
144
|
+
const { store, tick } = makeCounterStore()
|
|
145
|
+
const { code } = store.issue(makeEntry())
|
|
146
|
+
expect(store.exchange(code).ok).toBe(true) // consume it
|
|
147
|
+
expect(store.exchange(code)).toEqual({ ok: false, error: "consumed" }) // still in window
|
|
148
|
+
// ...but once past the TTL it is swept on the next activity and reads as
|
|
149
|
+
// "unknown" like any dead code — not "consumed" forever (MEDIUM-1 fix).
|
|
150
|
+
tick(60_001)
|
|
151
|
+
store.issue({ runnerId: "runner-2", token: "tok2.sig" }) // triggers sweep
|
|
152
|
+
expect(store.exchange(code)).toEqual({ ok: false, error: "unknown" })
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory single-use pairing code store (PR2 Stage 1).
|
|
3
|
+
*
|
|
4
|
+
* Issues short-lived, unguessable codes that each map once to a pre-minted
|
|
5
|
+
* runner credential. Codes are consumed atomically on first exchange; expired
|
|
6
|
+
* or unknown codes are rejected. Lost on server restart (the 10-min TTL window
|
|
7
|
+
* means the member can simply re-issue — EventStore persistence is deferred).
|
|
8
|
+
*
|
|
9
|
+
* Pure / injectable clock + RNG for unit tests.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Default pairing code TTL: 10 minutes. */
|
|
13
|
+
export const DEFAULT_CODE_TTL_MS = 10 * 60 * 1000
|
|
14
|
+
|
|
15
|
+
/** Base32 alphabet (RFC 4648, lowercase, human-legible, no padding). */
|
|
16
|
+
const BASE32_CHARS = "abcdefghijklmnopqrstuvwxyz234567"
|
|
17
|
+
|
|
18
|
+
/** Number of random bytes per code — gives ~80 bits of entropy. */
|
|
19
|
+
const CODE_RANDOM_BYTES = 10
|
|
20
|
+
|
|
21
|
+
/** Format: xxxxx-xxxxx-xxxxxx (5-5-6, 16 base32 chars total, dashed for readability). */
|
|
22
|
+
function formatCode(raw: string): string {
|
|
23
|
+
// 10 random bytes → 16 base32 chars; split 5-5-6 for human readability.
|
|
24
|
+
return `${raw.slice(0, 5)}-${raw.slice(5, 10)}-${raw.slice(10)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeBase32(bytes: Uint8Array): string {
|
|
28
|
+
let bits = 0
|
|
29
|
+
let value = 0
|
|
30
|
+
let output = ""
|
|
31
|
+
for (const byte of bytes) {
|
|
32
|
+
value = (value << 8) | byte
|
|
33
|
+
bits += 8
|
|
34
|
+
while (bits >= 5) {
|
|
35
|
+
bits -= 5
|
|
36
|
+
output += BASE32_CHARS[(value >>> bits) & 0x1f]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return output
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PairingEntry {
|
|
43
|
+
runnerId: string
|
|
44
|
+
/** Pre-minted durable runner credential token. Never logged in full. */
|
|
45
|
+
token: string
|
|
46
|
+
expiresAt: number
|
|
47
|
+
consumed: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ExchangeResult =
|
|
51
|
+
| { ok: true; runnerId: string; token: string }
|
|
52
|
+
| { ok: false; error: "expired" | "consumed" | "unknown" }
|
|
53
|
+
|
|
54
|
+
export interface PairingStoreOptions {
|
|
55
|
+
/** Injected for tests; defaults to Date.now. */
|
|
56
|
+
now?: () => number
|
|
57
|
+
/** Injected for tests; defaults to crypto.getRandomValues. */
|
|
58
|
+
randomBytes?: (n: number) => Uint8Array
|
|
59
|
+
/** Code TTL in ms; defaults to DEFAULT_CODE_TTL_MS (10 min). */
|
|
60
|
+
ttlMs?: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class PairingStore {
|
|
64
|
+
private readonly codes = new Map<string, PairingEntry>()
|
|
65
|
+
private readonly now: () => number
|
|
66
|
+
private readonly randomBytes: (n: number) => Uint8Array
|
|
67
|
+
private readonly ttlMs: number
|
|
68
|
+
|
|
69
|
+
constructor(options: PairingStoreOptions = {}) {
|
|
70
|
+
this.now = options.now ?? (() => Date.now())
|
|
71
|
+
this.randomBytes = options.randomBytes ?? ((n) => {
|
|
72
|
+
const buf = new Uint8Array(n)
|
|
73
|
+
crypto.getRandomValues(buf)
|
|
74
|
+
return buf
|
|
75
|
+
})
|
|
76
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_CODE_TTL_MS
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Issue a new pairing code for the given runner credential.
|
|
81
|
+
* Returns the code and the timestamp (ms) when it expires.
|
|
82
|
+
*/
|
|
83
|
+
issue(params: { runnerId: string; token: string }): { code: string; expiresAt: number } {
|
|
84
|
+
this.sweep()
|
|
85
|
+
const raw = encodeBase32(this.randomBytes(CODE_RANDOM_BYTES))
|
|
86
|
+
const code = formatCode(raw)
|
|
87
|
+
const expiresAt = this.now() + this.ttlMs
|
|
88
|
+
this.codes.set(code, { runnerId: params.runnerId, token: params.token, expiresAt, consumed: false })
|
|
89
|
+
return { code, expiresAt }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Exchange a code for its runner credential, atomically consuming it.
|
|
94
|
+
* A second call with the same code returns { error: "consumed" }.
|
|
95
|
+
* An expired code returns { error: "expired" }.
|
|
96
|
+
* An unknown code returns { error: "unknown" }.
|
|
97
|
+
*/
|
|
98
|
+
exchange(code: string): ExchangeResult {
|
|
99
|
+
// Look up the entry BEFORE sweeping so we can distinguish "expired"
|
|
100
|
+
// (we know the code, it just timed out) from "unknown" (never issued).
|
|
101
|
+
const entry = this.codes.get(code)
|
|
102
|
+
this.sweep()
|
|
103
|
+
if (!entry) return { ok: false, error: "unknown" }
|
|
104
|
+
if (entry.consumed) return { ok: false, error: "consumed" }
|
|
105
|
+
if (this.now() >= entry.expiresAt) return { ok: false, error: "expired" }
|
|
106
|
+
// Atomically mark consumed before returning.
|
|
107
|
+
entry.consumed = true
|
|
108
|
+
return { ok: true, runnerId: entry.runnerId, token: entry.token }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove expired entries (consumed OR not) to bound memory growth.
|
|
113
|
+
* Sweeping consumed-and-expired entries also avoids a permanent
|
|
114
|
+
* "consumed vs. unknown" distinction for long-dead codes — within the TTL
|
|
115
|
+
* window a reused code still reports "consumed" (the entry has not expired);
|
|
116
|
+
* after the window it is evicted and reads as "unknown" like any other.
|
|
117
|
+
*/
|
|
118
|
+
private sweep(): void {
|
|
119
|
+
const now = this.now()
|
|
120
|
+
for (const [code, entry] of this.codes) {
|
|
121
|
+
if (now >= entry.expiresAt) this.codes.delete(code)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
export function resolveLocalPath(localPath: string) {
|
|
6
|
+
const trimmed = localPath.trim()
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
throw new Error("Project path is required")
|
|
9
|
+
}
|
|
10
|
+
if (trimmed === "~") {
|
|
11
|
+
return homedir()
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.startsWith("~/")) {
|
|
14
|
+
return path.join(homedir(), trimmed.slice(2))
|
|
15
|
+
}
|
|
16
|
+
return path.resolve(trimmed)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureProjectDirectory(localPath: string) {
|
|
20
|
+
const resolvedPath = resolveLocalPath(localPath)
|
|
21
|
+
|
|
22
|
+
await mkdir(resolvedPath, { recursive: true })
|
|
23
|
+
const info = await stat(resolvedPath)
|
|
24
|
+
if (!info.isDirectory()) {
|
|
25
|
+
throw new Error("Project path must be a directory")
|
|
26
|
+
}
|
|
27
|
+
}
|