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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subject-scope policy for NATS auth-callout (Stage A, PR1).
|
|
3
|
+
*
|
|
4
|
+
* Three connection classes are defined in design.md; this module is the
|
|
5
|
+
* single source of truth for what each class may publish and subscribe.
|
|
6
|
+
* Pure — no I/O, fully unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The three connection classes defined by PR1. */
|
|
10
|
+
export type ConnectionClass = "server-admin" | "ui-client" | "runner"
|
|
11
|
+
|
|
12
|
+
/** Resolved identity, output of credential validation in the responder. */
|
|
13
|
+
export type ResolvedIdentity =
|
|
14
|
+
| { class: "server-admin" }
|
|
15
|
+
| { class: "ui-client" }
|
|
16
|
+
| { class: "runner"; runnerId: string }
|
|
17
|
+
|
|
18
|
+
/** Allow/deny lists for pub and sub that go into the signed user JWT. */
|
|
19
|
+
export interface SubjectScope {
|
|
20
|
+
pub: { allow: string[]; deny: string[] }
|
|
21
|
+
sub: { allow: string[]; deny: string[] }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── KV / JetStream subject patterns ─────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// KV write to a specific key in bucket B maps to:
|
|
27
|
+
// $KV.B.<key> — the client publishes to this subject
|
|
28
|
+
// KV reads and JetStream API calls use:
|
|
29
|
+
// $JS.API.> — broad JetStream API
|
|
30
|
+
// $KV.> — all KV subjects (reads/meta)
|
|
31
|
+
//
|
|
32
|
+
// For a runner scoped to its own registry key, we allow publish on its key
|
|
33
|
+
// and deny publish on any other key in the registry bucket.
|
|
34
|
+
|
|
35
|
+
/** KV bucket for runner registration (from runner-protocol.ts). */
|
|
36
|
+
const RUNNER_REGISTRY_BUCKET = "runtime_runner_registry"
|
|
37
|
+
|
|
38
|
+
/** Build the runner's own KV key subject. */
|
|
39
|
+
function runnerOwnKvSubject(runnerId: string): string {
|
|
40
|
+
return `$KV.${RUNNER_REGISTRY_BUCKET}.${runnerId}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// ── Policy factory ───────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the publish/subscribe allow-deny scope for a resolved identity.
|
|
48
|
+
* The caller signs these into a user JWT; NATS enforces them.
|
|
49
|
+
*/
|
|
50
|
+
export function permissionsFor(identity: ResolvedIdentity): SubjectScope {
|
|
51
|
+
switch (identity.class) {
|
|
52
|
+
case "server-admin":
|
|
53
|
+
return serverAdminScope()
|
|
54
|
+
case "ui-client":
|
|
55
|
+
return uiClientScope()
|
|
56
|
+
case "runner":
|
|
57
|
+
return runnerScope(identity.runnerId)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Per-class policies (match design.md exactly) ─────────────────────────────
|
|
62
|
+
|
|
63
|
+
function serverAdminScope(): SubjectScope {
|
|
64
|
+
return {
|
|
65
|
+
pub: {
|
|
66
|
+
allow: [
|
|
67
|
+
"runtime.>", // all runtime subjects
|
|
68
|
+
"$JS.API.>", // JetStream API
|
|
69
|
+
"$JS.ACK.>", // JetStream message acknowledgements (separate tree from $JS.API)
|
|
70
|
+
"$KV.>", // all KV operations
|
|
71
|
+
"_INBOX.>", // inbox replies
|
|
72
|
+
],
|
|
73
|
+
deny: [],
|
|
74
|
+
},
|
|
75
|
+
sub: {
|
|
76
|
+
allow: [
|
|
77
|
+
"runtime.>",
|
|
78
|
+
"$JS.API.>",
|
|
79
|
+
"$JS.ACK.>",
|
|
80
|
+
"$KV.>",
|
|
81
|
+
"_INBOX.>",
|
|
82
|
+
"$SYS.REQ.USER.AUTH", // callout responder subscription
|
|
83
|
+
],
|
|
84
|
+
deny: [],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function uiClientScope(): SubjectScope {
|
|
90
|
+
// ui-client may only publish to runtime.cmd.> (UI commands).
|
|
91
|
+
// runtime.runner.cmd.> is NOT in the allow list → denied by NATS by default.
|
|
92
|
+
// No deny list needed: NATS's allow-only model covers this.
|
|
93
|
+
//
|
|
94
|
+
// JetStream subjects: the browser calls js.consumers.get(KANNA_CHAT_MESSAGE_EVENTS, ...)
|
|
95
|
+
// which publishes to these $JS.API.* subjects (empirically confirmed in Stage D):
|
|
96
|
+
// $JS.API.STREAM.INFO.KANNA_CHAT_MESSAGE_EVENTS — stream lookup before consumer create
|
|
97
|
+
// $JS.API.CONSUMER.CREATE.KANNA_CHAT_MESSAGE_EVENTS — ephemeral consumer create
|
|
98
|
+
// $JS.API.CONSUMER.CREATE.KANNA_CHAT_MESSAGE_EVENTS.* — named variant (filter-subject path)
|
|
99
|
+
// $JS.API.CONSUMER.INFO.KANNA_CHAT_MESSAGE_EVENTS.* — status checks on reconnect
|
|
100
|
+
// $JS.API.CONSUMER.DELETE.KANNA_CHAT_MESSAGE_EVENTS.* — cleanup on close
|
|
101
|
+
//
|
|
102
|
+
// Scoped to KANNA_CHAT_MESSAGE_EVENTS only — the single stream the browser client
|
|
103
|
+
// accesses directly (see nats-socket.ts activateJetStreamConsumer). The browser
|
|
104
|
+
// never touches other KANNA_* streams or any admin API ($JS.API.STREAM.CREATE, etc).
|
|
105
|
+
return {
|
|
106
|
+
pub: {
|
|
107
|
+
allow: [
|
|
108
|
+
"runtime.cmd.>", // UI commands only (runner cmds NOT included)
|
|
109
|
+
"_INBOX.>",
|
|
110
|
+
// JetStream consumer lifecycle for KANNA_CHAT_MESSAGE_EVENTS only.
|
|
111
|
+
// When filter_subjects is set, the library appends both an ephemeral name
|
|
112
|
+
// AND the filter subject token, producing:
|
|
113
|
+
// $JS.API.CONSUMER.CREATE.KANNA_CHAT_MESSAGE_EVENTS.<name>.<filter>
|
|
114
|
+
// so we need ">" (rest-of-subject wildcard) not just "*" (single token).
|
|
115
|
+
// INFO and DELETE also use "<stream>.<name>" (two tokens past the stream).
|
|
116
|
+
"$JS.API.STREAM.INFO.KANNA_CHAT_MESSAGE_EVENTS",
|
|
117
|
+
"$JS.API.CONSUMER.CREATE.KANNA_CHAT_MESSAGE_EVENTS",
|
|
118
|
+
"$JS.API.CONSUMER.CREATE.KANNA_CHAT_MESSAGE_EVENTS.>",
|
|
119
|
+
"$JS.API.CONSUMER.INFO.KANNA_CHAT_MESSAGE_EVENTS.>",
|
|
120
|
+
"$JS.API.CONSUMER.DELETE.KANNA_CHAT_MESSAGE_EVENTS.>",
|
|
121
|
+
// Pull-consumer fetch: the browser pulls messages via
|
|
122
|
+
// $JS.API.CONSUMER.MSG.NEXT.<stream>.<consumer>
|
|
123
|
+
// This was missing, so every pull was denied (Publish Violation) and the
|
|
124
|
+
// browser could not read chat messages. Same per-stream scoping as above.
|
|
125
|
+
"$JS.API.CONSUMER.MSG.NEXT.KANNA_CHAT_MESSAGE_EVENTS.>",
|
|
126
|
+
// JetStream message acknowledgements for that consumer. $JS.ACK is a
|
|
127
|
+
// separate subject tree from $JS.API; without it the browser's consumer
|
|
128
|
+
// can't ack and stalls once max-ack-pending is hit. Scoped to the chat stream.
|
|
129
|
+
"$JS.ACK.KANNA_CHAT_MESSAGE_EVENTS.>",
|
|
130
|
+
],
|
|
131
|
+
deny: [],
|
|
132
|
+
},
|
|
133
|
+
sub: {
|
|
134
|
+
allow: [
|
|
135
|
+
"runtime.runner.evt.>", // runner events
|
|
136
|
+
"runtime.evt.>", // general events
|
|
137
|
+
"runtime.snap.>", // snapshots
|
|
138
|
+
"_INBOX.>",
|
|
139
|
+
],
|
|
140
|
+
deny: [],
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runnerScope(runnerId: string): SubjectScope {
|
|
146
|
+
const ownKv = runnerOwnKvSubject(runnerId)
|
|
147
|
+
|
|
148
|
+
// NATS permission evaluation: if a subject matches an allow pattern, it is
|
|
149
|
+
// allowed. If it matches a deny pattern, it is denied regardless of allow.
|
|
150
|
+
// So for runner isolation we use ALLOW-only lists (no deny needed for sub):
|
|
151
|
+
// - allow only the specific runnerId's cmd subject (not a wildcard)
|
|
152
|
+
// - this naturally excludes all other runner cmd subjects
|
|
153
|
+
//
|
|
154
|
+
// For KV writes (pub): allow the specific key, deny the bucket wildcard so
|
|
155
|
+
// other keys are blocked. The own key is listed in allow first — but NATS
|
|
156
|
+
// deny still wins when both match. We rely on a specific-allow narrower than
|
|
157
|
+
// the deny wildcard to NOT match each other: ownKv is the full key (no
|
|
158
|
+
// wildcard), and the deny is "$KV.BUCKET.>" which DOES match ownKv.
|
|
159
|
+
// So we can't use a wildcard deny for the KV bucket.
|
|
160
|
+
//
|
|
161
|
+
// Instead: allow ONLY the exact own KV key (specific allow), and do NOT add
|
|
162
|
+
// a deny wildcard. Other KV subjects are simply not in the allow list →
|
|
163
|
+
// denied by default (no-allow = deny in NATS).
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
pub: {
|
|
167
|
+
allow: [
|
|
168
|
+
`runtime.runner.heartbeat.${runnerId}`, // its own heartbeat
|
|
169
|
+
"runtime.runner.evt.>", // runner events (any chatId)
|
|
170
|
+
ownKv, // write its own registry key only
|
|
171
|
+
"_INBOX.>",
|
|
172
|
+
// STAGE D TODO: narrow to the minimal $JS.API.* subjects needed for
|
|
173
|
+
// kvm.open(RUNNER_REGISTRY_BUCKET) + kvStore.put(runnerId, ...).
|
|
174
|
+
// The server pre-creates the KV bucket so the runner never needs
|
|
175
|
+
// STREAM.CREATE. The remaining subjects (STREAM.INFO, CONSUMER.CREATE)
|
|
176
|
+
// are bucket-scoped but the library may use unpredictable subject forms.
|
|
177
|
+
// Confirmed empirically with -DV trace and narrow in Stage D.
|
|
178
|
+
// This is a PILOT allowance — a runner can publish to any JS API subject,
|
|
179
|
+
// including ones for other streams. Flag for Stage D security review.
|
|
180
|
+
"$JS.API.>",
|
|
181
|
+
// JetStream message acknowledgements (separate tree from $JS.API). The
|
|
182
|
+
// runner consumes coordination/oauth streams; without this its consumers
|
|
183
|
+
// can't ack and stall. Matches the existing broad $JS.API.> pilot grant.
|
|
184
|
+
"$JS.ACK.>",
|
|
185
|
+
],
|
|
186
|
+
deny: [],
|
|
187
|
+
},
|
|
188
|
+
sub: {
|
|
189
|
+
allow: [
|
|
190
|
+
`runtime.runner.cmd.${runnerId}.>`, // its own command subject only
|
|
191
|
+
"_INBOX.>",
|
|
192
|
+
"$JS.API.>", // JetStream reads / consumer create
|
|
193
|
+
`$KV.${RUNNER_REGISTRY_BUCKET}.>`, // KV reads (for its own entry)
|
|
194
|
+
],
|
|
195
|
+
deny: [],
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Helpers used by tests ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/** Return the KV key subject a runner should be able to write. */
|
|
203
|
+
export function runnerKvKeySubject(runnerId: string): string {
|
|
204
|
+
return runnerOwnKvSubject(runnerId)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Return the cmd subject for a given runnerId. */
|
|
208
|
+
export function runnerCmdWildcard(runnerId: string): string {
|
|
209
|
+
return `runtime.runner.cmd.${runnerId}.>`
|
|
210
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for stateless credential tokens (token.ts).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Basic mint + verify round-trip.
|
|
6
|
+
* - byteOffset bug regression: verify a token minted when the secret is a
|
|
7
|
+
* sub-array view (non-zero byteOffset) so WebCrypto uses the exact bytes.
|
|
8
|
+
* - exp (expiry): valid token accepted, expired token rejected,
|
|
9
|
+
* tampered exp rejected (exp is inside the HMAC'd payload).
|
|
10
|
+
* - Tokens without exp are rejected (no-legacy-format regression).
|
|
11
|
+
* - Malformed / missing tokens rejected.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect } from "bun:test"
|
|
15
|
+
import { mintCredentialToken, verifyCredentialToken } from "./token"
|
|
16
|
+
|
|
17
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function freshSecret(): Uint8Array {
|
|
20
|
+
const s = new Uint8Array(32)
|
|
21
|
+
crypto.getRandomValues(s)
|
|
22
|
+
return s
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return a Uint8Array backed by the same buffer as `parent` but with a
|
|
27
|
+
* non-zero byteOffset so `.buffer` spans more than just the 32 bytes we care
|
|
28
|
+
* about. This reproduces the pooled-Buffer scenario (Buffer.from pools memory).
|
|
29
|
+
*/
|
|
30
|
+
function subArrayView(data: Uint8Array): Uint8Array {
|
|
31
|
+
const padded = new Uint8Array(64)
|
|
32
|
+
padded.set(data, 16) // byteOffset=16 for the view we return
|
|
33
|
+
return padded.subarray(16, 48)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Round-trip ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("mintCredentialToken / verifyCredentialToken", () => {
|
|
39
|
+
test("server-admin token round-trips", async () => {
|
|
40
|
+
const secret = freshSecret()
|
|
41
|
+
const token = await mintCredentialToken({ class: "server-admin" }, secret)
|
|
42
|
+
const identity = await verifyCredentialToken(token, secret)
|
|
43
|
+
expect(identity).toEqual({ class: "server-admin" })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("ui-client token round-trips", async () => {
|
|
47
|
+
const secret = freshSecret()
|
|
48
|
+
const token = await mintCredentialToken({ class: "ui-client" }, secret)
|
|
49
|
+
const identity = await verifyCredentialToken(token, secret)
|
|
50
|
+
expect(identity).toEqual({ class: "ui-client" })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("runner token round-trips with runnerId", async () => {
|
|
54
|
+
const secret = freshSecret()
|
|
55
|
+
const token = await mintCredentialToken({ class: "runner", runnerId: "r-42" }, secret)
|
|
56
|
+
const identity = await verifyCredentialToken(token, secret)
|
|
57
|
+
expect(identity).toEqual({ class: "runner", runnerId: "r-42" })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ── byteOffset regression ───────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
test("minting with a sub-array secret (non-zero byteOffset) still verifies", async () => {
|
|
63
|
+
const rawSecret = freshSecret()
|
|
64
|
+
// subArrayView has byteOffset=16; rawSecret and subView hold the same 32 bytes.
|
|
65
|
+
const subView = subArrayView(rawSecret)
|
|
66
|
+
expect(subView.byteOffset).toBe(16)
|
|
67
|
+
|
|
68
|
+
// Mint with the sub-view; verify with the canonical flat copy — must succeed.
|
|
69
|
+
const token = await mintCredentialToken({ class: "ui-client" }, subView)
|
|
70
|
+
const flatSecret = rawSecret.slice() // byteOffset=0
|
|
71
|
+
const identity = await verifyCredentialToken(token, flatSecret)
|
|
72
|
+
expect(identity).toEqual({ class: "ui-client" })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("verifying with a sub-array secret (non-zero byteOffset) succeeds", async () => {
|
|
76
|
+
const rawSecret = freshSecret()
|
|
77
|
+
const subView = subArrayView(rawSecret)
|
|
78
|
+
expect(subView.byteOffset).toBe(16)
|
|
79
|
+
|
|
80
|
+
// Mint with the canonical flat copy; verify with the sub-view.
|
|
81
|
+
const token = await mintCredentialToken({ class: "server-admin" }, rawSecret.slice())
|
|
82
|
+
const identity = await verifyCredentialToken(token, subView)
|
|
83
|
+
expect(identity).toEqual({ class: "server-admin" })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ── exp (expiry) ────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
test("token with a future exp is accepted", async () => {
|
|
89
|
+
const secret = freshSecret()
|
|
90
|
+
// Default TTL is 30 days; token should be valid right now.
|
|
91
|
+
const token = await mintCredentialToken({ class: "ui-client" }, secret)
|
|
92
|
+
expect(await verifyCredentialToken(token, secret)).toEqual({ class: "ui-client" })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("token with an already-expired exp is rejected", async () => {
|
|
96
|
+
const secret = freshSecret()
|
|
97
|
+
// Pass ttlSeconds=-1 so exp = iat - 1 (already in the past).
|
|
98
|
+
const token = await mintCredentialToken({ class: "ui-client" }, secret, -1)
|
|
99
|
+
expect(await verifyCredentialToken(token, secret)).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("token with tampered exp is rejected (exp is inside HMAC'd payload)", async () => {
|
|
103
|
+
const secret = freshSecret()
|
|
104
|
+
// Mint a valid token, then decode and modify exp in the payload.
|
|
105
|
+
const token = await mintCredentialToken({ class: "ui-client" }, secret)
|
|
106
|
+
const [payloadB64, sigB64] = token.split(".")
|
|
107
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString())
|
|
108
|
+
// Extend the exp far into the future by tampering.
|
|
109
|
+
payload.exp = Math.floor(Date.now() / 1000) + 999999
|
|
110
|
+
const tamperedPayloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url")
|
|
111
|
+
const tamperedToken = `${tamperedPayloadB64}.${sigB64}`
|
|
112
|
+
// HMAC no longer matches the new payload → must be rejected.
|
|
113
|
+
expect(await verifyCredentialToken(tamperedToken, secret)).toBeNull()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("token without exp field is rejected (no legacy format)", async () => {
|
|
117
|
+
const secret = freshSecret()
|
|
118
|
+
// Craft a token manually without an exp field.
|
|
119
|
+
const payloadObj = { c: "ui-client", iat: Math.floor(Date.now() / 1000) }
|
|
120
|
+
const payloadJson = JSON.stringify(payloadObj)
|
|
121
|
+
const payloadB64 = Buffer.from(payloadJson).toString("base64url")
|
|
122
|
+
|
|
123
|
+
// Sign it legitimately so the HMAC is valid.
|
|
124
|
+
const key = await crypto.subtle.importKey(
|
|
125
|
+
"raw",
|
|
126
|
+
secret.buffer.slice(secret.byteOffset, secret.byteOffset + secret.byteLength),
|
|
127
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
128
|
+
false,
|
|
129
|
+
["sign"]
|
|
130
|
+
)
|
|
131
|
+
const rawSig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payloadJson))
|
|
132
|
+
const sigB64 = Buffer.from(new Uint8Array(rawSig)).toString("base64url")
|
|
133
|
+
const legacyToken = `${payloadB64}.${sigB64}`
|
|
134
|
+
|
|
135
|
+
expect(await verifyCredentialToken(legacyToken, secret)).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// ── Wrong secret ────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
test("wrong secret returns null", async () => {
|
|
141
|
+
const secret = freshSecret()
|
|
142
|
+
const other = freshSecret()
|
|
143
|
+
const token = await mintCredentialToken({ class: "server-admin" }, secret)
|
|
144
|
+
expect(await verifyCredentialToken(token, other)).toBeNull()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// ── Malformed inputs ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
test("undefined token returns null", async () => {
|
|
150
|
+
const secret = freshSecret()
|
|
151
|
+
expect(await verifyCredentialToken(undefined, secret)).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("token without dot separator returns null", async () => {
|
|
155
|
+
const secret = freshSecret()
|
|
156
|
+
expect(await verifyCredentialToken("nodothere", secret)).toBeNull()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("garbage token returns null", async () => {
|
|
160
|
+
const secret = freshSecret()
|
|
161
|
+
expect(await verifyCredentialToken("!!!.???", secret)).toBeNull()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless signed credential tokens (Stage B, PR1).
|
|
3
|
+
*
|
|
4
|
+
* Bridges the server process (token issuance) and the daemon child process
|
|
5
|
+
* (credential validation in the callout responder) without any shared
|
|
6
|
+
* in-memory state. The two processes share only the 32-byte token secret
|
|
7
|
+
* stored on disk (loaded by ensureCalloutKeys).
|
|
8
|
+
*
|
|
9
|
+
* Token format:
|
|
10
|
+
* base64url(JSON payload) + "." + base64url(HMAC-SHA256(secret, JSON payload))
|
|
11
|
+
*
|
|
12
|
+
* Payload shape:
|
|
13
|
+
* { c: ConnectionClass, r?: string, iat: number, exp: number }
|
|
14
|
+
* where r is runnerId (only for class "runner") and exp is expiry epoch seconds.
|
|
15
|
+
*
|
|
16
|
+
* Pure — no I/O, fully unit-testable.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ResolvedIdentity } from "./scope-policy"
|
|
20
|
+
|
|
21
|
+
/** Default TTL: 30 days in seconds (pilot-safe; per-class TTLs are PR2). */
|
|
22
|
+
const DEFAULT_TTL_SECONDS = 30 * 24 * 60 * 60 // 2592000
|
|
23
|
+
|
|
24
|
+
interface TokenPayload {
|
|
25
|
+
/** Connection class. */
|
|
26
|
+
c: "server-admin" | "ui-client" | "runner"
|
|
27
|
+
/** Runner ID — only present for class "runner". */
|
|
28
|
+
r?: string
|
|
29
|
+
/** Issued-at epoch seconds. */
|
|
30
|
+
iat: number
|
|
31
|
+
/** Expiry epoch seconds (iat + ttl). Tokens without exp are rejected. */
|
|
32
|
+
exp: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function b64urlEncode(bytes: Uint8Array): string {
|
|
36
|
+
return Buffer.from(bytes).toString("base64url")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function b64urlDecode(s: string): Uint8Array {
|
|
40
|
+
return new Uint8Array(Buffer.from(s, "base64url"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Return an ArrayBuffer view that covers exactly the bytes of the given Uint8Array.
|
|
44
|
+
* Uint8Array.prototype.buffer may be a pooled backing buffer with a non-zero
|
|
45
|
+
* byteOffset, which would cause WebCrypto to sign/verify the wrong bytes.
|
|
46
|
+
* We copy into a fresh ArrayBuffer so the type is unambiguously ArrayBuffer
|
|
47
|
+
* (not SharedArrayBuffer) and WebCrypto never sees stray pooled bytes. */
|
|
48
|
+
function exactBuffer(view: Uint8Array): ArrayBuffer {
|
|
49
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function hmacSign(secret: Uint8Array, data: string): Promise<Uint8Array> {
|
|
53
|
+
const key = await crypto.subtle.importKey(
|
|
54
|
+
"raw",
|
|
55
|
+
exactBuffer(secret),
|
|
56
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
57
|
+
false,
|
|
58
|
+
["sign"]
|
|
59
|
+
)
|
|
60
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data))
|
|
61
|
+
return new Uint8Array(sig)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function hmacVerify(secret: Uint8Array, data: string, sig: Uint8Array): Promise<boolean> {
|
|
65
|
+
const key = await crypto.subtle.importKey(
|
|
66
|
+
"raw",
|
|
67
|
+
exactBuffer(secret),
|
|
68
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
69
|
+
false,
|
|
70
|
+
["verify"]
|
|
71
|
+
)
|
|
72
|
+
return crypto.subtle.verify("HMAC", key, exactBuffer(sig), new TextEncoder().encode(data))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mint a credential token for the given identity.
|
|
77
|
+
* The token is self-contained and verifiable with only the shared secret.
|
|
78
|
+
*
|
|
79
|
+
* @param ttlSeconds — lifetime in seconds (default: 30 days). Per-class TTLs
|
|
80
|
+
* and token refresh are deferred to PR2; this param exists so tests can use
|
|
81
|
+
* short TTLs without touching the default.
|
|
82
|
+
*/
|
|
83
|
+
export async function mintCredentialToken(
|
|
84
|
+
identity: ResolvedIdentity,
|
|
85
|
+
secret: Uint8Array,
|
|
86
|
+
ttlSeconds: number = DEFAULT_TTL_SECONDS
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const iat = Math.floor(Date.now() / 1000)
|
|
89
|
+
const payload: TokenPayload = {
|
|
90
|
+
c: identity.class,
|
|
91
|
+
iat,
|
|
92
|
+
exp: iat + ttlSeconds,
|
|
93
|
+
...(identity.class === "runner" ? { r: identity.runnerId } : {}),
|
|
94
|
+
}
|
|
95
|
+
const payloadJson = JSON.stringify(payload)
|
|
96
|
+
const payloadB64 = b64urlEncode(new TextEncoder().encode(payloadJson))
|
|
97
|
+
const sig = await hmacSign(secret, payloadJson)
|
|
98
|
+
return `${payloadB64}.${b64urlEncode(sig)}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Verify a credential token and return the resolved identity.
|
|
103
|
+
* Returns null if the token is missing, malformed, has an invalid signature,
|
|
104
|
+
* or has expired. Tokens without an `exp` field are also rejected.
|
|
105
|
+
*/
|
|
106
|
+
export async function verifyCredentialToken(
|
|
107
|
+
token: string | undefined,
|
|
108
|
+
secret: Uint8Array
|
|
109
|
+
): Promise<ResolvedIdentity | null> {
|
|
110
|
+
if (!token) return null
|
|
111
|
+
|
|
112
|
+
const dotIdx = token.indexOf(".")
|
|
113
|
+
if (dotIdx === -1) return null
|
|
114
|
+
|
|
115
|
+
const payloadB64 = token.slice(0, dotIdx)
|
|
116
|
+
const sigB64 = token.slice(dotIdx + 1)
|
|
117
|
+
|
|
118
|
+
let payloadJson: string
|
|
119
|
+
try {
|
|
120
|
+
payloadJson = new TextDecoder().decode(b64urlDecode(payloadB64))
|
|
121
|
+
} catch {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let sig: Uint8Array
|
|
126
|
+
try {
|
|
127
|
+
sig = b64urlDecode(sigB64)
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const valid = await hmacVerify(secret, payloadJson, sig)
|
|
133
|
+
if (!valid) return null
|
|
134
|
+
|
|
135
|
+
let payload: TokenPayload
|
|
136
|
+
try {
|
|
137
|
+
payload = JSON.parse(payloadJson) as TokenPayload
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Reject tokens without exp (no persisted tokens exist; defensive against old format).
|
|
143
|
+
if (typeof payload.exp !== "number") return null
|
|
144
|
+
|
|
145
|
+
// Reject expired tokens.
|
|
146
|
+
const now = Math.floor(Date.now() / 1000)
|
|
147
|
+
if (now > payload.exp) return null
|
|
148
|
+
|
|
149
|
+
if (payload.c === "runner") {
|
|
150
|
+
if (!payload.r) return null
|
|
151
|
+
return { class: "runner", runnerId: payload.r }
|
|
152
|
+
}
|
|
153
|
+
if (payload.c === "server-admin") return { class: "server-admin" }
|
|
154
|
+
if (payload.c === "ui-client") return { class: "ui-client" }
|
|
155
|
+
|
|
156
|
+
return null
|
|
157
|
+
}
|