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,1108 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { APP_NAME, getRuntimeProfile, LOG_PREFIX } from "../shared/branding"
|
|
3
|
+
import { EventStore } from "./event-store"
|
|
4
|
+
import { discoverProjects, type DiscoveredProject } from "./discovery"
|
|
5
|
+
import { getMachineDisplayName } from "./machine-name"
|
|
6
|
+
import { TerminalManager } from "./terminal-manager"
|
|
7
|
+
import { UpdateManager } from "./update-manager"
|
|
8
|
+
import type { UpdateInstallAttemptResult } from "./cli-runtime"
|
|
9
|
+
import { NatsDaemonManager, type NatsDaemonReadiness } from "./nats-daemon-manager"
|
|
10
|
+
import { NatsConnector } from "./nats-connector"
|
|
11
|
+
import { generateAuthToken } from "./nats-auth"
|
|
12
|
+
import { requiresCalloutForBind } from "./nats-bind-guard"
|
|
13
|
+
import { readToken } from "../nats/nats-token"
|
|
14
|
+
import { ensureCalloutKeys } from "../nats/auth-callout/keys"
|
|
15
|
+
import { mintCredentialToken } from "../nats/auth-callout/token"
|
|
16
|
+
import { createNatsPublisher } from "./nats-publisher"
|
|
17
|
+
import { registerCommandResponders } from "./nats-responders"
|
|
18
|
+
import { registerPtyResponders } from "./pty-responders"
|
|
19
|
+
import { OAuthSettingsStore } from "./oauth-pool/oauth-settings-store"
|
|
20
|
+
import { OAuthTokenPool } from "./oauth-pool/oauth-token-pool"
|
|
21
|
+
import { registerOAuthResponders } from "./oauth-pool/oauth-responders"
|
|
22
|
+
import { ensureTerminalEventsStream, ensureChatMessageStream, ensureRunnerEventsStream, ensureWorkspaceCoordinationStream, ensureSandboxEventsStream, ensureRunnerRegistryBucket } from "./nats-streams"
|
|
23
|
+
import { RunnerManager, type RunnerReadiness } from "./runner-manager"
|
|
24
|
+
import { PairingStore } from "./pairing-store"
|
|
25
|
+
import { resolveRunnerPairingUrls } from "./runner-pairing-urls"
|
|
26
|
+
import { forwardClientLogs } from "./client-log-forwarder"
|
|
27
|
+
import { RunnerProxy } from "./runner-proxy"
|
|
28
|
+
import { RunnerRouter } from "./runner-router"
|
|
29
|
+
import { TranscriptConsumer } from "./transcript-consumer"
|
|
30
|
+
import type { AgentProvider, TranscriptEntry, SessionStatus } from "../shared/types"
|
|
31
|
+
import type { ClientCommand } from "../shared/protocol"
|
|
32
|
+
import { SessionOrchestrator } from "./orchestration"
|
|
33
|
+
import { SessionIndex } from "./session-index"
|
|
34
|
+
import { TranscriptSearchIndex } from "./transcript-search"
|
|
35
|
+
import { WorkspaceAgent } from "./workspace-agent"
|
|
36
|
+
import { createWorkspaceAgentRouter } from "./workspace-agent-routes"
|
|
37
|
+
import { SkillCache } from "./skill-discovery"
|
|
38
|
+
import { WorkspaceConfigManager } from "./workspace-config-manager"
|
|
39
|
+
import { WorkspaceDirectoryPolicy } from "./workspace-directory-policy"
|
|
40
|
+
import { RepoManager } from "./repo-manager"
|
|
41
|
+
import { GitClonePolicy } from "./git-clone-policy"
|
|
42
|
+
import { WorkflowStore } from "./workflow-store"
|
|
43
|
+
import { WorkflowEngine } from "./workflow-engine"
|
|
44
|
+
import { initVapid, PushSubscriptionStore, createPushRouter, sendPushToAll } from "./push-notifications"
|
|
45
|
+
import { BunDockerClient, SandboxManager } from "./sandbox-manager"
|
|
46
|
+
import { RuntimeRegistry } from "./runtime-registry"
|
|
47
|
+
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk"
|
|
48
|
+
import type { DiscoveredModel } from "../shared/runtime-types"
|
|
49
|
+
import { createExtensionRouter } from "./extension-router"
|
|
50
|
+
import { serverExtensions } from "./extensions.config"
|
|
51
|
+
import { DelegationCoordinator, type DelegationStore } from "./delegation-coordinator"
|
|
52
|
+
|
|
53
|
+
export interface StartServerOptions {
|
|
54
|
+
port?: number
|
|
55
|
+
host?: string
|
|
56
|
+
strictPort?: boolean
|
|
57
|
+
sandbox?: boolean
|
|
58
|
+
onMigrationProgress?: (message: string) => void
|
|
59
|
+
update?: {
|
|
60
|
+
version: string
|
|
61
|
+
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
62
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const NATS_WS_PROXY_BUFFER_LIMIT = 256
|
|
67
|
+
|
|
68
|
+
export type NatsWsProxyCounters = {
|
|
69
|
+
upgrades: number
|
|
70
|
+
upstreamOpen: number
|
|
71
|
+
upstreamError: number
|
|
72
|
+
upstreamClose: number
|
|
73
|
+
sendOnConnecting: number
|
|
74
|
+
bufferedFrames: number
|
|
75
|
+
bufferDrops: number
|
|
76
|
+
// Lazy-mode visibility: number of clients we replayed cached INFO to,
|
|
77
|
+
// and number of duplicate INFO frames we suppressed from the real upstream.
|
|
78
|
+
cacheReplay: number
|
|
79
|
+
firstUpstreamDropped: number
|
|
80
|
+
lazyHelloDeferred: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Strings or owned ArrayBuffers — never Bun's recv Buffer slices, which get
|
|
84
|
+
// reused out from under us.
|
|
85
|
+
type NatsWsBufferedFrame = string | ArrayBuffer
|
|
86
|
+
|
|
87
|
+
export type NatsWsProxyData = {
|
|
88
|
+
wsPort: number
|
|
89
|
+
upstream: WebSocket | null
|
|
90
|
+
ready: boolean
|
|
91
|
+
closed: boolean
|
|
92
|
+
buffer: NatsWsBufferedFrame[]
|
|
93
|
+
droppedSinceLastLog: number
|
|
94
|
+
openedAt: number
|
|
95
|
+
// Lazy mode: true between proxy.open and the first client frame.
|
|
96
|
+
// While true, we have NOT opened the upstream yet — the browser sees a
|
|
97
|
+
// synthetic INFO replayed from cache and is waiting to send its CONNECT.
|
|
98
|
+
awaitingHello: boolean
|
|
99
|
+
// Lazy mode: drop the first frame the upstream sends us once it opens.
|
|
100
|
+
// That frame is NATS's own INFO, which would be a duplicate of what the
|
|
101
|
+
// browser already received from cache.
|
|
102
|
+
skipFirstUpstreamFrame: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type IncomingWsMessage = string | Buffer<ArrayBuffer>
|
|
106
|
+
|
|
107
|
+
function toBufferedFrame(message: IncomingWsMessage): NatsWsBufferedFrame {
|
|
108
|
+
if (typeof message === "string") return message
|
|
109
|
+
const copy = new ArrayBuffer(message.byteLength)
|
|
110
|
+
new Uint8Array(copy).set(message)
|
|
111
|
+
return copy
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CachedNatsInfo {
|
|
115
|
+
bytes: Uint8Array
|
|
116
|
+
isBinary: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Warmup helper: opens a one-shot WebSocket to NATS and captures the very
|
|
120
|
+
// first frame (the INFO line) raw. We need this so the proxy can replay INFO
|
|
121
|
+
// to browser clients WITHOUT having to open an upstream connection first —
|
|
122
|
+
// which is what causes NATS's 2-second auth-timeout clock to start before
|
|
123
|
+
// the browser has had time to send CONNECT over a high-latency path.
|
|
124
|
+
export async function warmupCachedNatsInfo(
|
|
125
|
+
natsWsUrl: string,
|
|
126
|
+
timeoutMs: number = 5000,
|
|
127
|
+
): Promise<CachedNatsInfo | null> {
|
|
128
|
+
return await new Promise<CachedNatsInfo | null>((resolve) => {
|
|
129
|
+
let settled = false
|
|
130
|
+
let ws: WebSocket
|
|
131
|
+
const finish = (result: CachedNatsInfo | null) => {
|
|
132
|
+
if (settled) return
|
|
133
|
+
settled = true
|
|
134
|
+
clearTimeout(timer)
|
|
135
|
+
try { ws?.close() } catch (_err: unknown) { /* ignore close errors */ }
|
|
136
|
+
resolve(result)
|
|
137
|
+
}
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
console.warn(LOG_PREFIX, `nats-ws INFO warmup timed out after ${timeoutMs}ms`)
|
|
140
|
+
finish(null)
|
|
141
|
+
}, timeoutMs)
|
|
142
|
+
try {
|
|
143
|
+
ws = new WebSocket(natsWsUrl)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.warn(LOG_PREFIX, `nats-ws INFO warmup failed to open: ${err instanceof Error ? err.message : String(err)}`)
|
|
146
|
+
finish(null)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
ws.binaryType = "arraybuffer"
|
|
150
|
+
ws.onmessage = (event) => {
|
|
151
|
+
if (settled) return
|
|
152
|
+
if (typeof event.data === "string") {
|
|
153
|
+
finish({ bytes: new TextEncoder().encode(event.data), isBinary: false })
|
|
154
|
+
} else {
|
|
155
|
+
const ab = event.data as ArrayBuffer
|
|
156
|
+
const bytes = new Uint8Array(ab.byteLength)
|
|
157
|
+
bytes.set(new Uint8Array(ab))
|
|
158
|
+
finish({ bytes, isBinary: true })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
ws.onerror = () => {
|
|
162
|
+
console.warn(LOG_PREFIX, "nats-ws INFO warmup WebSocket error")
|
|
163
|
+
finish(null)
|
|
164
|
+
}
|
|
165
|
+
ws.onclose = () => finish(null)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createNatsWsProxyHandlers(
|
|
170
|
+
counters: NatsWsProxyCounters = {
|
|
171
|
+
upgrades: 0,
|
|
172
|
+
upstreamOpen: 0,
|
|
173
|
+
upstreamError: 0,
|
|
174
|
+
upstreamClose: 0,
|
|
175
|
+
sendOnConnecting: 0,
|
|
176
|
+
bufferedFrames: 0,
|
|
177
|
+
bufferDrops: 0,
|
|
178
|
+
cacheReplay: 0,
|
|
179
|
+
firstUpstreamDropped: 0,
|
|
180
|
+
lazyHelloDeferred: 0,
|
|
181
|
+
},
|
|
182
|
+
cachedInfo: CachedNatsInfo | null = null,
|
|
183
|
+
) {
|
|
184
|
+
const lazyMode = cachedInfo !== null
|
|
185
|
+
|
|
186
|
+
function openUpstream(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
|
|
187
|
+
const upstream = new WebSocket(`ws://127.0.0.1:${ws.data.wsPort}`)
|
|
188
|
+
upstream.binaryType = "arraybuffer"
|
|
189
|
+
ws.data.upstream = upstream
|
|
190
|
+
|
|
191
|
+
upstream.onopen = () => {
|
|
192
|
+
if (ws.data.closed) {
|
|
193
|
+
upstream.close()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
const elapsed = Date.now() - ws.data.openedAt
|
|
197
|
+
ws.data.ready = true
|
|
198
|
+
counters.upstreamOpen += 1
|
|
199
|
+
console.warn(LOG_PREFIX, `nats-ws proxy upstream open t=${elapsed}ms`)
|
|
200
|
+
|
|
201
|
+
const pending = ws.data.buffer
|
|
202
|
+
ws.data.buffer = []
|
|
203
|
+
for (const frame of pending) {
|
|
204
|
+
upstream.send(frame)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
upstream.onmessage = (event) => {
|
|
209
|
+
if (ws.readyState !== WebSocket.OPEN) return
|
|
210
|
+
// Lazy mode: drop the very first frame from the upstream — it's NATS's
|
|
211
|
+
// own INFO, which the browser already received from the cache replay at
|
|
212
|
+
// proxy.open. Forwarding it would give the browser a second INFO with a
|
|
213
|
+
// different client_id mid-handshake, which nats-core would treat as a
|
|
214
|
+
// cluster update and could cause it to second-guess the active inbox sub.
|
|
215
|
+
if (ws.data.skipFirstUpstreamFrame) {
|
|
216
|
+
ws.data.skipFirstUpstreamFrame = false
|
|
217
|
+
counters.firstUpstreamDropped += 1
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (typeof event.data === "string") {
|
|
221
|
+
ws.sendText(event.data)
|
|
222
|
+
} else {
|
|
223
|
+
ws.sendBinary(new Uint8Array(event.data as ArrayBuffer))
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
upstream.onclose = (event) => {
|
|
228
|
+
counters.upstreamClose += 1
|
|
229
|
+
const duration = Date.now() - ws.data.openedAt
|
|
230
|
+
console.warn(
|
|
231
|
+
LOG_PREFIX,
|
|
232
|
+
`nats-ws proxy upstream close code=${event.code} duration=${duration}ms`,
|
|
233
|
+
)
|
|
234
|
+
if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
upstream.onerror = () => {
|
|
238
|
+
counters.upstreamError += 1
|
|
239
|
+
console.warn(LOG_PREFIX, "nats-ws proxy upstream error")
|
|
240
|
+
if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const handlers = {
|
|
245
|
+
open(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
|
|
246
|
+
counters.upgrades += 1
|
|
247
|
+
ws.data.openedAt = Date.now()
|
|
248
|
+
ws.data.ready = false
|
|
249
|
+
ws.data.closed = false
|
|
250
|
+
ws.data.buffer = []
|
|
251
|
+
ws.data.droppedSinceLastLog = 0
|
|
252
|
+
ws.data.awaitingHello = false
|
|
253
|
+
ws.data.skipFirstUpstreamFrame = false
|
|
254
|
+
|
|
255
|
+
if (lazyMode && cachedInfo) {
|
|
256
|
+
// Lazy path: replay cached INFO so the browser's nats-core advances
|
|
257
|
+
// its state machine and prepares to send CONNECT. Defer opening the
|
|
258
|
+
// upstream until the browser actually sends its first frame, so
|
|
259
|
+
// NATS's auth-timeout clock starts only when CONNECT is in our hand.
|
|
260
|
+
console.warn(LOG_PREFIX, "nats-ws proxy upgrade accepted (lazy)")
|
|
261
|
+
ws.data.awaitingHello = true
|
|
262
|
+
ws.data.skipFirstUpstreamFrame = true
|
|
263
|
+
counters.lazyHelloDeferred += 1
|
|
264
|
+
try {
|
|
265
|
+
if (cachedInfo.isBinary) {
|
|
266
|
+
ws.sendBinary(cachedInfo.bytes)
|
|
267
|
+
} else {
|
|
268
|
+
ws.sendText(new TextDecoder().decode(cachedInfo.bytes))
|
|
269
|
+
}
|
|
270
|
+
counters.cacheReplay += 1
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.warn(
|
|
273
|
+
LOG_PREFIX,
|
|
274
|
+
`nats-ws proxy failed to replay cached INFO: ${err instanceof Error ? err.message : String(err)}`,
|
|
275
|
+
)
|
|
276
|
+
ws.close()
|
|
277
|
+
}
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Eager fallback: original behavior used when warmup didn't produce a
|
|
282
|
+
// cache. Logs loudly so the operator knows the auth-race protection is
|
|
283
|
+
// disabled for this run.
|
|
284
|
+
console.warn(LOG_PREFIX, "nats-ws proxy upgrade accepted (eager — no INFO cache)")
|
|
285
|
+
openUpstream(ws)
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
message(ws: import("bun").ServerWebSocket<NatsWsProxyData>, message: IncomingWsMessage) {
|
|
289
|
+
if (ws.data.awaitingHello) {
|
|
290
|
+
// First client frame in lazy mode. Open the upstream NOW and put this
|
|
291
|
+
// frame at the head of the buffer — it'll be the first thing flushed
|
|
292
|
+
// when upstream's onopen fires, so NATS receives CONNECT within one
|
|
293
|
+
// localhost roundtrip of starting its auth clock.
|
|
294
|
+
ws.data.awaitingHello = false
|
|
295
|
+
counters.bufferedFrames += 1
|
|
296
|
+
ws.data.buffer.push(toBufferedFrame(message))
|
|
297
|
+
openUpstream(ws)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const upstream = ws.data.upstream
|
|
302
|
+
if (ws.data.ready && upstream && upstream.readyState === WebSocket.OPEN) {
|
|
303
|
+
upstream.send(message)
|
|
304
|
+
ws.data.droppedSinceLastLog = 0
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
counters.sendOnConnecting += 1
|
|
309
|
+
counters.bufferedFrames += 1
|
|
310
|
+
ws.data.buffer.push(toBufferedFrame(message))
|
|
311
|
+
|
|
312
|
+
if (ws.data.buffer.length > NATS_WS_PROXY_BUFFER_LIMIT) {
|
|
313
|
+
ws.data.buffer.shift()
|
|
314
|
+
counters.bufferDrops += 1
|
|
315
|
+
ws.data.droppedSinceLastLog += 1
|
|
316
|
+
if (ws.data.droppedSinceLastLog === 1) {
|
|
317
|
+
console.warn(
|
|
318
|
+
LOG_PREFIX,
|
|
319
|
+
`nats-ws proxy buffer overflow — dropping oldest frame (limit=${NATS_WS_PROXY_BUFFER_LIMIT})`,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
close(ws: import("bun").ServerWebSocket<NatsWsProxyData>) {
|
|
325
|
+
ws.data.closed = true
|
|
326
|
+
const duration = Date.now() - ws.data.openedAt
|
|
327
|
+
console.warn(LOG_PREFIX, `nats-ws proxy client close duration=${duration}ms`)
|
|
328
|
+
ws.data.buffer = []
|
|
329
|
+
ws.data.upstream?.close()
|
|
330
|
+
ws.data.upstream = null
|
|
331
|
+
ws.data.ready = false
|
|
332
|
+
ws.data.awaitingHello = false
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
return { handlers, counters }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface ServerHealthcheck {
|
|
339
|
+
ok: boolean
|
|
340
|
+
status: "ok" | "degraded" | "fail"
|
|
341
|
+
port: number
|
|
342
|
+
natsWsPort: number
|
|
343
|
+
natsDaemon: NatsDaemonReadiness | null
|
|
344
|
+
natsConnection: ReturnType<NatsConnector["getReadiness"]>
|
|
345
|
+
runner: RunnerReadiness
|
|
346
|
+
/** PR5: all registered runners from the KV fleet. Only present in /health responses. Empty array when NATS is not ready. */
|
|
347
|
+
runners?: import("./runner-router").RunnerDescriptor[]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Session coordinator interface — satisfied by RunnerProxy which delegates turn execution to the runner process. */
|
|
351
|
+
interface SessionCoordinator {
|
|
352
|
+
send(command: Extract<ClientCommand, { type: "chat.send" }>): Promise<{ chatId: string }>
|
|
353
|
+
queue(command: Extract<ClientCommand, { type: "chat.queue" }>): Promise<{ chatId: string; queued: boolean }>
|
|
354
|
+
drainQueuedTurn(chatId: string): Promise<boolean>
|
|
355
|
+
cancel(chatId: string): Promise<void>
|
|
356
|
+
respondTool(command: Extract<ClientCommand, { type: "chat.respondTool" }>): Promise<void>
|
|
357
|
+
disposeChat(chatId: string): Promise<void>
|
|
358
|
+
getActiveStatuses(): Map<string, SessionStatus>
|
|
359
|
+
activeTurns: { has(key: string): boolean }
|
|
360
|
+
startTurnForChat(args: {
|
|
361
|
+
chatId: string; provider: AgentProvider; content: string
|
|
362
|
+
delegatedContext?: string; isSpawned?: boolean; model: string; effort?: string
|
|
363
|
+
serviceTier?: "fast"; planMode: boolean; appendUserPrompt: boolean
|
|
364
|
+
}): Promise<void>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function startServer(options: StartServerOptions = {}) {
|
|
368
|
+
const port = options.port ?? 3210
|
|
369
|
+
const hostname = options.host ?? "127.0.0.1"
|
|
370
|
+
const strictPort = options.strictPort ?? false
|
|
371
|
+
const store = new EventStore()
|
|
372
|
+
const machineDisplayName = getMachineDisplayName()
|
|
373
|
+
await store.initialize()
|
|
374
|
+
await store.migrateLegacyTranscripts(options.onMigrationProgress)
|
|
375
|
+
let discoveredProjects: DiscoveredProject[] = []
|
|
376
|
+
|
|
377
|
+
async function refreshDiscovery() {
|
|
378
|
+
discoveredProjects = discoverProjects()
|
|
379
|
+
return discoveredProjects
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await refreshDiscovery()
|
|
383
|
+
|
|
384
|
+
let server: ReturnType<typeof Bun.serve>
|
|
385
|
+
const terminals = new TerminalManager()
|
|
386
|
+
const updateManager = options.update
|
|
387
|
+
? new UpdateManager({
|
|
388
|
+
currentVersion: options.update.version,
|
|
389
|
+
fetchLatestVersion: options.update.fetchLatestVersion,
|
|
390
|
+
installVersion: options.update.installVersion,
|
|
391
|
+
devMode: getRuntimeProfile() === "dev",
|
|
392
|
+
})
|
|
393
|
+
: null
|
|
394
|
+
|
|
395
|
+
const natsMode = process.env.NATS_MODE ?? "embedded"
|
|
396
|
+
const authMode = process.env.NATS_AUTH_MODE ?? "callout"
|
|
397
|
+
const runnerMode = process.env.RUNNER_MODE ?? "spawn"
|
|
398
|
+
|
|
399
|
+
// Guard: a non-loopback bind is only safe in callout mode (decision 0007).
|
|
400
|
+
// Token mode must stay loopback-only — a shared-token bus must not be exposed
|
|
401
|
+
// beyond the local machine. Callout mode provides per-connection scoped creds,
|
|
402
|
+
// so WireGuard on the tailnet is sufficient for confidentiality (no TLS needed).
|
|
403
|
+
const bindGuard = requiresCalloutForBind(hostname, authMode as "callout" | "token")
|
|
404
|
+
if (!bindGuard.ok) {
|
|
405
|
+
throw new Error(bindGuard.reason!)
|
|
406
|
+
}
|
|
407
|
+
if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1" && authMode === "callout") {
|
|
408
|
+
console.warn(LOG_PREFIX, `Binding NATS to ${hostname} in callout mode — confidentiality via WireGuard, WS no_tls within the tailnet`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let authToken: string
|
|
412
|
+
let daemonManager: NatsDaemonManager
|
|
413
|
+
let daemonInfo: { url: string; wsUrl: string; wsPort: number }
|
|
414
|
+
|
|
415
|
+
// Callout mode: stateless signed tokens per connection class.
|
|
416
|
+
// Token mode (NATS_AUTH_MODE=token): shared static token — legacy escape hatch.
|
|
417
|
+
let uiClientToken: string // returned by /auth/token for browser WS connections
|
|
418
|
+
let mintRunnerToken: ((runnerId: string) => Promise<string>) | undefined
|
|
419
|
+
// Durable runner credential mint for pairing (callout mode only).
|
|
420
|
+
// RUNNER_PAIR_TTL: long-lived token TTL in seconds (default 90 days).
|
|
421
|
+
// Paired runners persist this token and present it at every NATS connect.
|
|
422
|
+
const runnerPairTtl = Number(process.env.RUNNER_PAIR_TTL ?? 7_776_000)
|
|
423
|
+
let mintPairedRunnerToken: ((runnerId: string) => Promise<string>) | undefined
|
|
424
|
+
|
|
425
|
+
if (natsMode === "external") {
|
|
426
|
+
const natsUrl = process.env.NATS_URL
|
|
427
|
+
const natsWsPort = Number(process.env.NATS_WS_PORT)
|
|
428
|
+
const natsDataDir = process.env.NATS_DATA_DIR
|
|
429
|
+
if (!natsUrl || !natsWsPort || !natsDataDir) {
|
|
430
|
+
throw new Error("NATS_MODE=external requires NATS_URL, NATS_WS_PORT, and NATS_DATA_DIR")
|
|
431
|
+
}
|
|
432
|
+
authToken = await readToken(natsDataDir)
|
|
433
|
+
uiClientToken = authToken
|
|
434
|
+
daemonManager = NatsDaemonManager.fromExternal({ natsUrl, wsPort: natsWsPort })
|
|
435
|
+
const url = new URL(natsUrl)
|
|
436
|
+
daemonInfo = { url: natsUrl, wsUrl: `ws://${url.hostname}:${natsWsPort}`, wsPort: natsWsPort }
|
|
437
|
+
console.warn(LOG_PREFIX, `NATS_MODE=external — connecting to ${natsUrl}`)
|
|
438
|
+
} else if (authMode === "callout") {
|
|
439
|
+
// Callout mode: load (or generate) signing keys + shared token secret.
|
|
440
|
+
const natsDataDir = process.env.NATS_DATA_DIR ?? store.dataDir
|
|
441
|
+
// The callout daemon child (spawned by ensureDaemon) loads its keys + token
|
|
442
|
+
// secret from NATS_DATA_DIR and refuses to start without it. ensureDaemon
|
|
443
|
+
// reads it from the environment, so default the env to the resolved dir here
|
|
444
|
+
// — otherwise a normal `tinkaria` run (no NATS_DATA_DIR set) fails to boot.
|
|
445
|
+
process.env.NATS_DATA_DIR ??= natsDataDir
|
|
446
|
+
const keys = await ensureCalloutKeys(natsDataDir)
|
|
447
|
+
|
|
448
|
+
authToken = await mintCredentialToken({ class: "server-admin" }, keys.tokenSecret)
|
|
449
|
+
uiClientToken = await mintCredentialToken({ class: "ui-client" }, keys.tokenSecret)
|
|
450
|
+
mintRunnerToken = (runnerId: string) =>
|
|
451
|
+
mintCredentialToken({ class: "runner", runnerId }, keys.tokenSecret)
|
|
452
|
+
// Paired runners get a long-lived token (RUNNER_PAIR_TTL, default 90d).
|
|
453
|
+
mintPairedRunnerToken = (runnerId: string) =>
|
|
454
|
+
mintCredentialToken({ class: "runner", runnerId }, keys.tokenSecret, runnerPairTtl)
|
|
455
|
+
|
|
456
|
+
daemonManager = NatsDaemonManager.embedded()
|
|
457
|
+
const info = await daemonManager.ensureDaemon({ token: authToken, host: hostname })
|
|
458
|
+
daemonInfo = info
|
|
459
|
+
console.warn(LOG_PREFIX, `NATS_AUTH_MODE=callout — scoped credentials active`)
|
|
460
|
+
} else {
|
|
461
|
+
// Token mode (legacy escape hatch): shared static token, token-mode daemon.
|
|
462
|
+
authToken = generateAuthToken()
|
|
463
|
+
uiClientToken = authToken
|
|
464
|
+
daemonManager = NatsDaemonManager.embedded()
|
|
465
|
+
const info = await daemonManager.ensureDaemon({ token: authToken, host: hostname })
|
|
466
|
+
daemonInfo = info
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Pairing code store (callout mode only — see POST /api/pairing/code).
|
|
470
|
+
const pairingStore = new PairingStore()
|
|
471
|
+
|
|
472
|
+
// Push notifications
|
|
473
|
+
initVapid()
|
|
474
|
+
const pushStore = new PushSubscriptionStore(path.join(store.dataDir, "push-subscriptions.json"))
|
|
475
|
+
await pushStore.load()
|
|
476
|
+
const pushRouter = createPushRouter(pushStore)
|
|
477
|
+
|
|
478
|
+
const natsConnector = await NatsConnector.connect({
|
|
479
|
+
natsUrl: daemonInfo.url,
|
|
480
|
+
natsWsUrl: daemonInfo.wsUrl,
|
|
481
|
+
natsWsPort: daemonInfo.wsPort,
|
|
482
|
+
token: authToken,
|
|
483
|
+
})
|
|
484
|
+
await Promise.all([
|
|
485
|
+
ensureTerminalEventsStream(natsConnector.nc),
|
|
486
|
+
ensureChatMessageStream(natsConnector.nc),
|
|
487
|
+
ensureRunnerEventsStream(natsConnector.nc),
|
|
488
|
+
ensureWorkspaceCoordinationStream(natsConnector.nc),
|
|
489
|
+
ensureSandboxEventsStream(natsConnector.nc),
|
|
490
|
+
// Pre-create the runner registry KV bucket so spawned runners (whose
|
|
491
|
+
// callout scope excludes STREAM.CREATE) can open it immediately.
|
|
492
|
+
ensureRunnerRegistryBucket(natsConnector.nc),
|
|
493
|
+
])
|
|
494
|
+
|
|
495
|
+
const getHealthcheck = (): ServerHealthcheck => {
|
|
496
|
+
const natsDaemon = daemonManager.getReadiness()
|
|
497
|
+
const natsConnection = natsConnector.getReadiness()
|
|
498
|
+
const runner = runnerManager.getReadiness()
|
|
499
|
+
const hardFailure = !natsDaemon?.ok || !natsConnection.ok || !runner.ok
|
|
500
|
+
return {
|
|
501
|
+
ok: !hardFailure,
|
|
502
|
+
status: hardFailure ? "fail" : "ok",
|
|
503
|
+
port: actualPort,
|
|
504
|
+
natsWsPort: natsConnector.natsWsPort,
|
|
505
|
+
natsDaemon,
|
|
506
|
+
natsConnection,
|
|
507
|
+
runner,
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Project agent: cross-session awareness and coordination
|
|
512
|
+
const sessionIndex = new SessionIndex()
|
|
513
|
+
const transcriptSearch = new TranscriptSearchIndex()
|
|
514
|
+
const projectAgent = new WorkspaceAgent({
|
|
515
|
+
sessions: sessionIndex,
|
|
516
|
+
store,
|
|
517
|
+
search: transcriptSearch,
|
|
518
|
+
workspaceId: "",
|
|
519
|
+
})
|
|
520
|
+
const projectAgentRouter = createWorkspaceAgentRouter(projectAgent)
|
|
521
|
+
const extensionRouter = createExtensionRouter(serverExtensions)
|
|
522
|
+
|
|
523
|
+
// Use indirection to break the circular dependency:
|
|
524
|
+
// coordinator -> onStateChange -> publisher.broadcastSnapshots
|
|
525
|
+
// publisher -> coordinator.getActiveStatuses
|
|
526
|
+
//
|
|
527
|
+
// Debounce: during streaming, dozens of events arrive per second.
|
|
528
|
+
// Each calls onStateChange() which would trigger broadcastSnapshots().
|
|
529
|
+
// Coalesce into one broadcast per microtask tick using queueMicrotask.
|
|
530
|
+
// Selective invalidation: only recompute topic types that changed.
|
|
531
|
+
let broadcastPending = false
|
|
532
|
+
const pendingTypes = new Set<string>()
|
|
533
|
+
let broadcastFn: (changedTypes?: ReadonlySet<string>) => void = () => {}
|
|
534
|
+
const broadcast = (changedTypes?: ReadonlySet<string>) => {
|
|
535
|
+
if (changedTypes) {
|
|
536
|
+
for (const t of changedTypes) pendingTypes.add(t)
|
|
537
|
+
}
|
|
538
|
+
if (broadcastPending) return
|
|
539
|
+
broadcastPending = true
|
|
540
|
+
queueMicrotask(() => {
|
|
541
|
+
broadcastPending = false
|
|
542
|
+
const types = pendingTypes.size > 0 ? new Set(pendingTypes) : undefined
|
|
543
|
+
pendingTypes.clear()
|
|
544
|
+
broadcastFn(types)
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
let publishMessage: (chatId: string, entry: TranscriptEntry) => void = () => {}
|
|
548
|
+
let reconcileDelegation: (workspaceId: string, chatId: string, outcome: "success" | "failed" | "cancelled") => void = () => {}
|
|
549
|
+
|
|
550
|
+
const onMessageAppended = (chatId: string, entry: TranscriptEntry) => {
|
|
551
|
+
publishMessage(chatId, entry)
|
|
552
|
+
sessionIndex.onMessageAppended(chatId, entry, store.state)
|
|
553
|
+
transcriptSearch.addEntry(chatId, entry)
|
|
554
|
+
orchestrator.onMessageAppended(chatId, entry)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const skillCache = new SkillCache()
|
|
558
|
+
|
|
559
|
+
// ── Runner process handles all turn execution ──
|
|
560
|
+
const runnerManager = new RunnerManager({
|
|
561
|
+
nc: natsConnector.nc,
|
|
562
|
+
natsUrl: daemonInfo.url,
|
|
563
|
+
authToken,
|
|
564
|
+
mintToken: mintRunnerToken,
|
|
565
|
+
mode: runnerMode as "spawn" | "discover",
|
|
566
|
+
})
|
|
567
|
+
const runnerId = await runnerManager.ensureRunner()
|
|
568
|
+
|
|
569
|
+
const chatSidebarTypes = new Set(["chat", "sidebar", "orchestration"])
|
|
570
|
+
const previousStatuses = new Map<string, string>()
|
|
571
|
+
const transcriptConsumer = new TranscriptConsumer({
|
|
572
|
+
nc: natsConnector.nc,
|
|
573
|
+
store,
|
|
574
|
+
onStateChange: () => {
|
|
575
|
+
broadcast(chatSidebarTypes)
|
|
576
|
+
// Push notification triggers
|
|
577
|
+
const currentActive = transcriptConsumer.getActiveStatuses()
|
|
578
|
+
// Check for newly waiting_for_user
|
|
579
|
+
for (const [chatId, status] of currentActive) {
|
|
580
|
+
const prev = previousStatuses.get(chatId)
|
|
581
|
+
if (status === "waiting_for_user" && prev !== "waiting_for_user") {
|
|
582
|
+
const chat = store.state.chatsById.get(chatId)
|
|
583
|
+
void sendPushToAll(pushStore, {
|
|
584
|
+
title: "Input needed",
|
|
585
|
+
body: chat?.title || chatId,
|
|
586
|
+
url: `/chat/${chatId}`,
|
|
587
|
+
tag: `waiting-${chatId}`,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Check for chats that were active but are now gone (turn ended)
|
|
592
|
+
for (const [chatId, prevStatus] of previousStatuses) {
|
|
593
|
+
if (!currentActive.has(chatId) && prevStatus !== "waiting_for_user") {
|
|
594
|
+
const chat = store.state.chatsById.get(chatId)
|
|
595
|
+
const outcome = chat?.lastTurnOutcome
|
|
596
|
+
if (outcome === "success") {
|
|
597
|
+
void sendPushToAll(pushStore, {
|
|
598
|
+
title: "Agent finished",
|
|
599
|
+
body: chat?.title || chatId,
|
|
600
|
+
url: `/chat/${chatId}`,
|
|
601
|
+
tag: `turn-${chatId}`,
|
|
602
|
+
})
|
|
603
|
+
} else if (outcome === "failed") {
|
|
604
|
+
void sendPushToAll(pushStore, {
|
|
605
|
+
title: "Agent failed",
|
|
606
|
+
body: chat?.title || chatId,
|
|
607
|
+
url: `/chat/${chatId}`,
|
|
608
|
+
tag: `turn-${chatId}`,
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Delegation reconciliation: if this chat was a delegation child, reconcile and possibly resume the parent
|
|
613
|
+
const chatRecord = store.state.chatsById.get(chatId)
|
|
614
|
+
if (chatRecord?.lastTurnOutcome) {
|
|
615
|
+
reconcileDelegation(chatRecord.workspaceId, chatId, chatRecord.lastTurnOutcome)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
void coordinator.drainQueuedTurn(chatId).catch((error) => {
|
|
619
|
+
console.warn(LOG_PREFIX, "queued chat drain failed:", error instanceof Error ? error.message : String(error))
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Sync previousStatuses
|
|
624
|
+
previousStatuses.clear()
|
|
625
|
+
for (const [chatId, status] of currentActive) {
|
|
626
|
+
previousStatuses.set(chatId, status)
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
onMessageAppended,
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const runtimeRegistry = new RuntimeRegistry(path.join(store.dataDir, "runtimes"), {
|
|
633
|
+
probeClaudeModels: async (binaryPath: string): Promise<DiscoveredModel[]> => {
|
|
634
|
+
const q = sdkQuery({
|
|
635
|
+
prompt: "",
|
|
636
|
+
options: {
|
|
637
|
+
pathToClaudeCodeExecutable: binaryPath,
|
|
638
|
+
tools: [],
|
|
639
|
+
permissionMode: "bypassPermissions",
|
|
640
|
+
persistSession: false,
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
let timer: ReturnType<typeof setTimeout>
|
|
644
|
+
try {
|
|
645
|
+
const models = await Promise.race([
|
|
646
|
+
q.supportedModels(),
|
|
647
|
+
new Promise<never>((_, reject) => {
|
|
648
|
+
timer = setTimeout(() => reject(new Error("Model probe timed out")), 15_000)
|
|
649
|
+
}),
|
|
650
|
+
])
|
|
651
|
+
return models.map((m) => ({
|
|
652
|
+
value: m.value,
|
|
653
|
+
displayName: m.displayName,
|
|
654
|
+
description: m.description,
|
|
655
|
+
supportsEffort: m.supportsEffort,
|
|
656
|
+
supportedEffortLevels: m.supportedEffortLevels,
|
|
657
|
+
supportsAdaptiveThinking: m.supportsAdaptiveThinking,
|
|
658
|
+
supportsFastMode: m.supportsFastMode,
|
|
659
|
+
supportsAutoMode: m.supportsAutoMode,
|
|
660
|
+
}))
|
|
661
|
+
} finally {
|
|
662
|
+
clearTimeout(timer!)
|
|
663
|
+
try { q.close() } catch (_) { /* close may fail on timed-out probes */ }
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
})
|
|
667
|
+
await runtimeRegistry.initialize()
|
|
668
|
+
|
|
669
|
+
const runnerRouter = new RunnerRouter({
|
|
670
|
+
nc: natsConnector.nc,
|
|
671
|
+
sharedRunnerId: () => {
|
|
672
|
+
try { return runnerManager.getRunnerId() } catch { return null }
|
|
673
|
+
},
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
const coordinator: SessionCoordinator = new RunnerProxy({
|
|
677
|
+
nc: natsConnector.nc,
|
|
678
|
+
store,
|
|
679
|
+
runnerId,
|
|
680
|
+
getActiveStatuses: () => transcriptConsumer.getActiveStatuses(),
|
|
681
|
+
runtimeRegistry,
|
|
682
|
+
router: runnerRouter,
|
|
683
|
+
sharedRunnerId: () => runnerManager.getRunnerId(),
|
|
684
|
+
getRunnerReadiness: (targetRunnerId: string) => {
|
|
685
|
+
// For the shared/local runner, use the authoritative RunnerManager readiness.
|
|
686
|
+
// For personal runners, we keep the gate synchronous by failing closed when
|
|
687
|
+
// the descriptor has not been pre-fetched. resolveRunnerForChat already
|
|
688
|
+
// called router.select (which calls router.list), but that descriptor is not
|
|
689
|
+
// cached here. To avoid making getRunnerReadiness async (which would ripple
|
|
690
|
+
// into sendCommand and every call-site), we apply a conservative policy:
|
|
691
|
+
// the shared runner uses its live readiness; a selected personal runner
|
|
692
|
+
// passes the gate (fail-open for the readiness check — the eligibleFor()
|
|
693
|
+
// filter in selectFrom already enforced liveness + compat before selection,
|
|
694
|
+
// so a selected runner is already known-compatible at the time of dispatch).
|
|
695
|
+
if (targetRunnerId === runnerId) {
|
|
696
|
+
return runnerManager.getReadiness()
|
|
697
|
+
}
|
|
698
|
+
// Personal runner: trust that router.select already enforced
|
|
699
|
+
// liveness + protocol compat via eligibleFor(). Return compatible.
|
|
700
|
+
return { incompatible: false, protocolVersion: null, capabilities: null }
|
|
701
|
+
},
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
console.warn(LOG_PREFIX, "Runner process handles turn execution")
|
|
705
|
+
|
|
706
|
+
// ── Durable delegation coordinator ──
|
|
707
|
+
const delegationStoreAdapter: DelegationStore = {
|
|
708
|
+
appendMessage: (chatId, entry) => store.appendMessage(chatId, entry),
|
|
709
|
+
chatExists: (chatId) => {
|
|
710
|
+
const chat = store.state.chatsById.get(chatId)
|
|
711
|
+
return chat != null && chat.deletedAt == null
|
|
712
|
+
},
|
|
713
|
+
getChatWorkspaceId: (chatId) => store.state.chatsById.get(chatId)?.workspaceId,
|
|
714
|
+
getLastTurnOutcome: (chatId) => store.state.chatsById.get(chatId)?.lastTurnOutcome ?? undefined,
|
|
715
|
+
}
|
|
716
|
+
const delegationCoordinator = new DelegationCoordinator(natsConnector.nc, delegationStoreAdapter)
|
|
717
|
+
await delegationCoordinator.initialize()
|
|
718
|
+
await delegationCoordinator.bootReconciliation()
|
|
719
|
+
|
|
720
|
+
const orchestrator = new SessionOrchestrator({
|
|
721
|
+
store,
|
|
722
|
+
coordinator,
|
|
723
|
+
delegationCoordinator,
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
// Start after orchestrator exists — onMessageAppended closes over it
|
|
727
|
+
await transcriptConsumer.start()
|
|
728
|
+
|
|
729
|
+
const publisher = await createNatsPublisher({
|
|
730
|
+
nc: natsConnector.nc,
|
|
731
|
+
store,
|
|
732
|
+
agent: coordinator,
|
|
733
|
+
terminals,
|
|
734
|
+
refreshDiscovery,
|
|
735
|
+
getDiscoveredProjects: () => discoveredProjects,
|
|
736
|
+
machineDisplayName,
|
|
737
|
+
updateManager,
|
|
738
|
+
skillCache,
|
|
739
|
+
orchestrator,
|
|
740
|
+
runtimeRegistry,
|
|
741
|
+
hasActiveBlockingDelegations: delegationCoordinator.hasActiveBlockingDelegations.bind(delegationCoordinator),
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
broadcastFn = (types) => publisher.broadcastSnapshots(types)
|
|
745
|
+
publishMessage = (chatId, entry) => publisher.publishChatMessage(chatId, entry)
|
|
746
|
+
reconcileDelegation = (workspaceId, chatId, outcome) => {
|
|
747
|
+
void delegationCoordinator
|
|
748
|
+
.reconcileChildTerminal(workspaceId, chatId, { outcome })
|
|
749
|
+
.then((result) => {
|
|
750
|
+
if (result && !("alreadyReconciled" in result) && result.resumeEligible) {
|
|
751
|
+
void coordinator.drainQueuedTurn(result.parentChatId).catch((err) => {
|
|
752
|
+
console.warn(LOG_PREFIX, "delegation parent drain failed:", err instanceof Error ? err.message : String(err))
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
})
|
|
756
|
+
.catch((err) => {
|
|
757
|
+
console.warn(LOG_PREFIX, "delegation reconciliation failed:", err instanceof Error ? err.message : String(err))
|
|
758
|
+
})
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const responders = registerCommandResponders({
|
|
762
|
+
nc: natsConnector.nc,
|
|
763
|
+
store,
|
|
764
|
+
agent: coordinator,
|
|
765
|
+
terminals,
|
|
766
|
+
refreshDiscovery,
|
|
767
|
+
updateManager,
|
|
768
|
+
publisher,
|
|
769
|
+
onStateChange: () => publisher.broadcastSnapshots(),
|
|
770
|
+
repoManager: new RepoManager(),
|
|
771
|
+
clonePolicy: new GitClonePolicy(store, new RepoManager(), () => publisher.broadcastSnapshots()),
|
|
772
|
+
directoryPolicy: new WorkspaceDirectoryPolicy(
|
|
773
|
+
store,
|
|
774
|
+
new WorkspaceConfigManager(path.join(store.dataDir, "workspaces")),
|
|
775
|
+
() => publisher.broadcastSnapshots(),
|
|
776
|
+
),
|
|
777
|
+
workflowStore: new WorkflowStore(path.join(store.dataDir, "workflows")),
|
|
778
|
+
workflowEngine: new WorkflowEngine({
|
|
779
|
+
emitter: store,
|
|
780
|
+
dispatcher: { dispatch: async () => "" },
|
|
781
|
+
resolveRepos: async (workspaceId: string) => {
|
|
782
|
+
return [...store.state.reposById.values()]
|
|
783
|
+
.filter((r) => r.workspaceId === workspaceId)
|
|
784
|
+
.map((r) => r.id)
|
|
785
|
+
},
|
|
786
|
+
onProgress: () => publisher.broadcastSnapshots(),
|
|
787
|
+
}),
|
|
788
|
+
sandboxManager: options.sandbox ? new SandboxManager(new BunDockerClient(), "nats://host.docker.internal:4222") : null,
|
|
789
|
+
runtimeRegistry,
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
const oauthSettings = new OAuthSettingsStore()
|
|
793
|
+
await oauthSettings.load()
|
|
794
|
+
const oauthPool = new OAuthTokenPool(
|
|
795
|
+
() => oauthSettings.getTokens(),
|
|
796
|
+
(id, patch) => oauthSettings.mutateTokenStatus(id, patch),
|
|
797
|
+
Date.now,
|
|
798
|
+
() => oauthSettings.getConcurrencyDefault(),
|
|
799
|
+
)
|
|
800
|
+
const oauthResponders = registerOAuthResponders({ nc: natsConnector.nc, store: oauthSettings })
|
|
801
|
+
|
|
802
|
+
const ptyResponders = registerPtyResponders({ nc: natsConnector.nc, store, oauthPool })
|
|
803
|
+
|
|
804
|
+
// Boot-time self-test: round-trip pty.snapshot through NATS to prove the
|
|
805
|
+
// responder is reachable. Logs a single line so /health smoke can confirm.
|
|
806
|
+
;(async () => {
|
|
807
|
+
try {
|
|
808
|
+
const { ptyCommandSubject } = await import("../shared/nats-subjects")
|
|
809
|
+
const { compressPayload, decompressPayload } = await import("../shared/compression")
|
|
810
|
+
const enc = new TextEncoder()
|
|
811
|
+
const dec = new TextDecoder()
|
|
812
|
+
const reply = await natsConnector.nc.request(
|
|
813
|
+
ptyCommandSubject("pty.snapshot"),
|
|
814
|
+
compressPayload(enc.encode("{}")),
|
|
815
|
+
{ timeout: 2000 },
|
|
816
|
+
)
|
|
817
|
+
const text = dec.decode(await decompressPayload(reply.data))
|
|
818
|
+
console.log(LOG_PREFIX, "claude-pty self-test pty.snapshot:", text)
|
|
819
|
+
|
|
820
|
+
// Spawn smoke: prove driver invocation reaches verifyPtyAuth by issuing
|
|
821
|
+
// pty.spawn with no OAuth token. Expected reply: {ok:false, error:"…OAuth pool token…"}.
|
|
822
|
+
const spawnReply = await natsConnector.nc.request(
|
|
823
|
+
ptyCommandSubject("pty.spawn"),
|
|
824
|
+
compressPayload(enc.encode(JSON.stringify({
|
|
825
|
+
chatId: "smoke-spawn-no-token",
|
|
826
|
+
projectId: "smoke",
|
|
827
|
+
cwd: process.cwd(),
|
|
828
|
+
model: "opus",
|
|
829
|
+
oauthToken: null,
|
|
830
|
+
oauthLabel: "smoke-test",
|
|
831
|
+
}))),
|
|
832
|
+
{ timeout: 2000 },
|
|
833
|
+
)
|
|
834
|
+
const spawnText = dec.decode(await decompressPayload(spawnReply.data))
|
|
835
|
+
console.log(LOG_PREFIX, "claude-pty self-test pty.spawn (no token):", spawnText)
|
|
836
|
+
|
|
837
|
+
// Self-test spawned a real PTY when pool had tokens — kill it so the
|
|
838
|
+
// smoke-spawn-no-token instance does not leak into every chat's PTY
|
|
839
|
+
// indicator (publishes `removed` delta to client store).
|
|
840
|
+
try {
|
|
841
|
+
await natsConnector.nc.request(
|
|
842
|
+
ptyCommandSubject("pty.exit"),
|
|
843
|
+
compressPayload(enc.encode(JSON.stringify({ chatId: "smoke-spawn-no-token" }))),
|
|
844
|
+
{ timeout: 2000 },
|
|
845
|
+
)
|
|
846
|
+
} catch {
|
|
847
|
+
// Best-effort cleanup; ignore failures (instance may not exist if
|
|
848
|
+
// spawn rejected due to empty pool).
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const { oauthCommandSubject } = await import("../shared/nats-subjects")
|
|
852
|
+
const oauthListReply = await natsConnector.nc.request(
|
|
853
|
+
oauthCommandSubject("oauth.list"),
|
|
854
|
+
compressPayload(enc.encode("{}")),
|
|
855
|
+
{ timeout: 2000 },
|
|
856
|
+
)
|
|
857
|
+
const oauthListText = dec.decode(await decompressPayload(oauthListReply.data))
|
|
858
|
+
console.log(LOG_PREFIX, "oauth-pool self-test oauth.list:", oauthListText)
|
|
859
|
+
} catch (err) {
|
|
860
|
+
console.warn(LOG_PREFIX, "claude-pty self-test failed:", err instanceof Error ? err.message : String(err))
|
|
861
|
+
}
|
|
862
|
+
})().catch(() => undefined)
|
|
863
|
+
|
|
864
|
+
const distDir = path.join(import.meta.dir, "..", "..", "dist", "client")
|
|
865
|
+
|
|
866
|
+
// Warm up the synthetic INFO cache used by the lazy-upstream proxy path.
|
|
867
|
+
// See createNatsWsProxyHandlers / warmupCachedNatsInfo for why this exists.
|
|
868
|
+
// If warmup fails we fall back to eager mode (the legacy auth-race path).
|
|
869
|
+
const warmupUrl = `ws://127.0.0.1:${natsConnector.natsWsPort}`
|
|
870
|
+
const cachedNatsInfo = await warmupCachedNatsInfo(warmupUrl)
|
|
871
|
+
if (cachedNatsInfo) {
|
|
872
|
+
console.warn(
|
|
873
|
+
LOG_PREFIX,
|
|
874
|
+
`nats-ws INFO cache warmed (${cachedNatsInfo.bytes.length} bytes, ${cachedNatsInfo.isBinary ? "binary" : "text"})`,
|
|
875
|
+
)
|
|
876
|
+
} else {
|
|
877
|
+
console.warn(
|
|
878
|
+
LOG_PREFIX,
|
|
879
|
+
"nats-ws INFO cache UNAVAILABLE — proxy will fall back to eager upstream (auth-race risk on slow networks)",
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const { handlers: natsWsHandlers, counters: natsWsCounters } = createNatsWsProxyHandlers(undefined, cachedNatsInfo)
|
|
884
|
+
const natsWsCounterInterval = setInterval(() => {
|
|
885
|
+
const hasActivity = Object.values(natsWsCounters).some((v) => v > 0)
|
|
886
|
+
if (!hasActivity) return
|
|
887
|
+
console.warn(LOG_PREFIX, "nats-ws proxy counters:", JSON.stringify(natsWsCounters))
|
|
888
|
+
for (const key of Object.keys(natsWsCounters) as (keyof NatsWsProxyCounters)[]) {
|
|
889
|
+
natsWsCounters[key] = 0
|
|
890
|
+
}
|
|
891
|
+
}, 60_000)
|
|
892
|
+
|
|
893
|
+
const MAX_PORT_ATTEMPTS = 20
|
|
894
|
+
let actualPort = port
|
|
895
|
+
|
|
896
|
+
for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
|
|
897
|
+
try {
|
|
898
|
+
server = Bun.serve<NatsWsProxyData>({
|
|
899
|
+
port: actualPort,
|
|
900
|
+
hostname,
|
|
901
|
+
async fetch(req, srv) {
|
|
902
|
+
const url = new URL(req.url)
|
|
903
|
+
|
|
904
|
+
if (url.pathname === "/nats-ws") {
|
|
905
|
+
const upgraded = srv.upgrade(req, {
|
|
906
|
+
data: {
|
|
907
|
+
wsPort: natsConnector.natsWsPort,
|
|
908
|
+
upstream: null,
|
|
909
|
+
ready: false,
|
|
910
|
+
closed: false,
|
|
911
|
+
buffer: [],
|
|
912
|
+
droppedSinceLastLog: 0,
|
|
913
|
+
openedAt: 0,
|
|
914
|
+
awaitingHello: false,
|
|
915
|
+
skipFirstUpstreamFrame: false,
|
|
916
|
+
},
|
|
917
|
+
})
|
|
918
|
+
if (upgraded) return undefined
|
|
919
|
+
return new Response("WebSocket upgrade failed", { status: 426 })
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (url.pathname === "/health") {
|
|
923
|
+
const healthcheck = getHealthcheck()
|
|
924
|
+
// runners is async (KV list); fetch in parallel with the sync healthcheck.
|
|
925
|
+
// Failures return [] so the existing ok/status logic is never blocked.
|
|
926
|
+
const runners = await runnerRouter.list().catch(() => [])
|
|
927
|
+
return Response.json({ ...healthcheck, runners }, {
|
|
928
|
+
status: healthcheck.ok ? 200 : 503,
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (url.pathname === "/auth/token") {
|
|
933
|
+
const advertisedHost = process.env.NATS_ADVERTISED_HOST
|
|
934
|
+
const natsWsUrl = advertisedHost
|
|
935
|
+
? `ws://${advertisedHost}:${natsConnector.natsWsPort}`
|
|
936
|
+
: undefined
|
|
937
|
+
// In callout mode: return the ui-client scoped token (not the server-admin token).
|
|
938
|
+
// In token mode: uiClientToken === authToken — same behaviour as before.
|
|
939
|
+
return Response.json({
|
|
940
|
+
token: uiClientToken,
|
|
941
|
+
...(natsWsUrl ? { natsWsUrl } : {}),
|
|
942
|
+
})
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ── Pairing endpoints (PR2) ─────────────────────────────────────────
|
|
946
|
+
//
|
|
947
|
+
// Both routes are public (no user auth), matching /auth/token's posture.
|
|
948
|
+
// The code itself is the bearer secret — short TTL, single-use, unguessable.
|
|
949
|
+
// Gating on user-auth is the documented pre-multi-tenant follow-up.
|
|
950
|
+
//
|
|
951
|
+
if (url.pathname === "/api/pairing/code" && req.method === "POST") {
|
|
952
|
+
// Only available in callout mode — the minted token is a callout credential.
|
|
953
|
+
if (!mintPairedRunnerToken) {
|
|
954
|
+
return Response.json(
|
|
955
|
+
{ error: "pairing requires NATS_AUTH_MODE=callout" },
|
|
956
|
+
{ status: 409 }
|
|
957
|
+
)
|
|
958
|
+
}
|
|
959
|
+
// Allocate a runnerId (same format as runner-manager's spawned runners).
|
|
960
|
+
const newRunnerId = `runner-${Date.now()}-${process.pid}`
|
|
961
|
+
const token = await mintPairedRunnerToken(newRunnerId)
|
|
962
|
+
const { code, expiresAt } = pairingStore.issue({ runnerId: newRunnerId, token })
|
|
963
|
+
console.warn(LOG_PREFIX, `Pairing code issued for runner ${newRunnerId}, expires ${new Date(expiresAt).toISOString()}`)
|
|
964
|
+
return Response.json({ code, expiresAt })
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (url.pathname === "/api/pairing/exchange" && req.method === "POST") {
|
|
968
|
+
let body: { code?: unknown }
|
|
969
|
+
try {
|
|
970
|
+
body = await req.json() as { code?: unknown }
|
|
971
|
+
} catch {
|
|
972
|
+
return Response.json({ error: "invalid JSON body" }, { status: 400 })
|
|
973
|
+
}
|
|
974
|
+
const code = body?.code
|
|
975
|
+
if (typeof code !== "string" || !code.trim()) {
|
|
976
|
+
return Response.json({ error: "missing or invalid code" }, { status: 400 })
|
|
977
|
+
}
|
|
978
|
+
const result = pairingStore.exchange(code)
|
|
979
|
+
if (!result.ok) {
|
|
980
|
+
const isGone = result.error === "expired" || result.error === "consumed"
|
|
981
|
+
console.warn(LOG_PREFIX, `Pairing exchange rejected: ${result.error}`)
|
|
982
|
+
return Response.json({ error: result.error }, { status: isGone ? 410 : 400 })
|
|
983
|
+
}
|
|
984
|
+
// A paired runner may be on another machine, so the credential's
|
|
985
|
+
// NATS URL host must be routable from there — never the wildcard
|
|
986
|
+
// bind host (0.0.0.0 / ::), which would resolve to the runner's own
|
|
987
|
+
// loopback and yield ECONNREFUSED. Honor NATS_ADVERTISED_HOST (same
|
|
988
|
+
// as /auth/token); refuse to hand out an unroutable URL otherwise.
|
|
989
|
+
const pairingUrls = resolveRunnerPairingUrls(daemonInfo)
|
|
990
|
+
if (!pairingUrls.ok) {
|
|
991
|
+
console.warn(LOG_PREFIX, `Pairing exchange blocked for runner ${result.runnerId}: ${pairingUrls.error}`)
|
|
992
|
+
return Response.json({ error: pairingUrls.error }, { status: 409 })
|
|
993
|
+
}
|
|
994
|
+
console.warn(LOG_PREFIX, `Pairing exchange succeeded for runner ${result.runnerId}`)
|
|
995
|
+
return Response.json({
|
|
996
|
+
runnerId: result.runnerId,
|
|
997
|
+
token: result.token,
|
|
998
|
+
natsUrl: pairingUrls.natsUrl,
|
|
999
|
+
natsWsUrl: pairingUrls.natsWsUrl,
|
|
1000
|
+
})
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ── Client log ingest (decision 0012) ──────────────────────────────
|
|
1004
|
+
// Browser ships console logs + errors here; we forward to VictoriaLogs
|
|
1005
|
+
// (localhost-only) so the browser never touches VL directly. Always
|
|
1006
|
+
// 204 — logging must never error for the client, even if VL is down.
|
|
1007
|
+
if (url.pathname === "/api/logs" && req.method === "POST") {
|
|
1008
|
+
let body: unknown
|
|
1009
|
+
try {
|
|
1010
|
+
body = await req.json()
|
|
1011
|
+
} catch {
|
|
1012
|
+
return new Response(null, { status: 204 })
|
|
1013
|
+
}
|
|
1014
|
+
void forwardClientLogs(body)
|
|
1015
|
+
return new Response(null, { status: 204 })
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (url.pathname.startsWith("/api/workspace/")) {
|
|
1019
|
+
return projectAgentRouter(req)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (url.pathname.startsWith("/api/ext/")) {
|
|
1023
|
+
return extensionRouter(req)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (url.pathname.startsWith("/api/push/")) {
|
|
1027
|
+
return pushRouter(req)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return serveStatic(distDir, url.pathname)
|
|
1031
|
+
},
|
|
1032
|
+
websocket: natsWsHandlers,
|
|
1033
|
+
})
|
|
1034
|
+
break
|
|
1035
|
+
} catch (err: unknown) {
|
|
1036
|
+
const isAddrInUse =
|
|
1037
|
+
err instanceof Error && "code" in err && (err as NodeJS.ErrnoException).code === "EADDRINUSE"
|
|
1038
|
+
if (!isAddrInUse || strictPort || attempt === MAX_PORT_ATTEMPTS - 1) {
|
|
1039
|
+
throw err
|
|
1040
|
+
}
|
|
1041
|
+
console.log(`Port ${actualPort} is in use, trying ${actualPort + 1}...`)
|
|
1042
|
+
actualPort++
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
console.warn(LOG_PREFIX, `Operational health initialized — status: ${getHealthcheck().status}`)
|
|
1047
|
+
|
|
1048
|
+
const shutdown = async () => {
|
|
1049
|
+
clearInterval(natsWsCounterInterval)
|
|
1050
|
+
orchestrator.destroy()
|
|
1051
|
+
responders.dispose()
|
|
1052
|
+
ptyResponders.dispose()
|
|
1053
|
+
oauthResponders.dispose()
|
|
1054
|
+
publisher.dispose()
|
|
1055
|
+
terminals.closeAll()
|
|
1056
|
+
transcriptConsumer.stop()
|
|
1057
|
+
await runnerManager.dispose()
|
|
1058
|
+
await natsConnector.dispose()
|
|
1059
|
+
await daemonManager.dispose()
|
|
1060
|
+
await store.compact()
|
|
1061
|
+
server.stop(true)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
port: actualPort,
|
|
1066
|
+
store,
|
|
1067
|
+
updateManager,
|
|
1068
|
+
healthcheck: getHealthcheck,
|
|
1069
|
+
stop: shutdown,
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function serveStatic(distDir: string, pathname: string) {
|
|
1074
|
+
const requestedPath = pathname === "/" ? "/index.html" : pathname
|
|
1075
|
+
const filePath = path.join(distDir, requestedPath)
|
|
1076
|
+
const indexPath = path.join(distDir, "index.html")
|
|
1077
|
+
|
|
1078
|
+
const file = Bun.file(filePath)
|
|
1079
|
+
if (await file.exists()) {
|
|
1080
|
+
const isHashedAsset = requestedPath.startsWith("/assets/")
|
|
1081
|
+
const isHtml = requestedPath.endsWith(".html")
|
|
1082
|
+
const cacheControl = isHashedAsset
|
|
1083
|
+
? "public, max-age=31536000, immutable"
|
|
1084
|
+
: isHtml
|
|
1085
|
+
? "no-cache"
|
|
1086
|
+
: undefined
|
|
1087
|
+
|
|
1088
|
+
const headers: Record<string, string> = {}
|
|
1089
|
+
if (cacheControl) headers["Cache-Control"] = cacheControl
|
|
1090
|
+
|
|
1091
|
+
return new Response(file, { headers })
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const indexFile = Bun.file(indexPath)
|
|
1095
|
+
if (await indexFile.exists()) {
|
|
1096
|
+
return new Response(indexFile, {
|
|
1097
|
+
headers: {
|
|
1098
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1099
|
+
"Cache-Control": "no-cache",
|
|
1100
|
+
},
|
|
1101
|
+
})
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return new Response(
|
|
1105
|
+
`${APP_NAME} client bundle not found. Run \`bun run build\` inside workbench/ first.`,
|
|
1106
|
+
{ status: 503 }
|
|
1107
|
+
)
|
|
1108
|
+
}
|