@crouton-kit/crouter 0.3.32 → 0.3.33
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/README.md +8 -0
- package/dist/build-root.js +1 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
- package/dist/builtin-memory/crouter-development/personas.md +3 -4
- package/dist/builtin-memory/crouter-development/plugins.md +18 -18
- package/dist/builtin-memory/design.md +4 -1
- package/dist/builtin-memory/development.md +4 -1
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
- package/dist/builtin-memory/internal/storage-tiers.md +5 -5
- package/dist/builtin-memory/planning.md +4 -1
- package/dist/builtin-memory/product.md +80 -0
- package/dist/builtin-memory/spec.md +4 -1
- package/dist/builtin-personas/advisor/PERSONA.md +10 -0
- package/dist/builtin-personas/design/PERSONA.md +2 -2
- package/dist/builtin-personas/design/orchestrator.md +3 -3
- package/dist/builtin-personas/developer/PERSONA.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +2 -2
- package/dist/builtin-personas/explore/PERSONA.md +3 -3
- package/dist/builtin-personas/explore/orchestrator.md +4 -2
- package/dist/builtin-personas/general/PERSONA.md +1 -1
- package/dist/builtin-personas/general/orchestrator.md +1 -1
- package/dist/builtin-personas/orchestration-kernel.md +7 -14
- package/dist/builtin-personas/plan/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/orchestrator.md +2 -2
- package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
- package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
- package/dist/builtin-personas/product/PERSONA.md +18 -0
- package/dist/builtin-personas/product/orchestrator.md +14 -0
- package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
- package/dist/builtin-personas/review/PERSONA.md +1 -1
- package/dist/builtin-personas/review/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +5 -0
- package/dist/builtin-personas/spec/PERSONA.md +1 -1
- package/dist/builtin-personas/spec/orchestrator.md +2 -2
- package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
- package/dist/builtin-views/canvas/core.mjs +82 -1
- package/dist/builtin-views/canvas/tui.mjs +9 -5
- package/dist/builtin-views/canvas/web.jsx +3 -2
- package/dist/builtin-views/chat/core.mjs +725 -0
- package/dist/builtin-views/chat/text.mjs +101 -0
- package/dist/builtin-views/chat/tui.mjs +368 -0
- package/dist/builtin-views/chat/web.jsx +367 -0
- package/dist/builtin-views/prompt-review/core.mjs +863 -0
- package/dist/builtin-views/prompt-review/text.mjs +15 -0
- package/dist/builtin-views/prompt-review/tui.mjs +196 -0
- package/dist/builtin-views/prompt-review/web.jsx +484 -0
- package/dist/builtin-views/settings/core.mjs +397 -0
- package/dist/builtin-views/settings/text.mjs +40 -0
- package/dist/builtin-views/settings/tui.mjs +95 -0
- package/dist/builtin-views/settings/web.jsx +167 -0
- package/dist/cli.js +16 -3
- package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
- package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
- package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
- package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
- package/dist/clients/attach/attach-cmd.d.ts +18 -0
- package/dist/clients/attach/attach-cmd.js +1723 -795
- package/dist/clients/attach/canvas-panels.js +2 -2
- package/dist/clients/attach/chat-view.d.ts +10 -0
- package/dist/clients/attach/chat-view.js +23 -0
- package/dist/clients/attach/config-load.d.ts +8 -4
- package/dist/clients/attach/config-load.js +2 -0
- package/dist/clients/attach/extension-dialogs.d.ts +2 -3
- package/dist/clients/attach/extension-dialogs.js +7 -9
- package/dist/clients/attach/graph-overlay.js +3 -2
- package/dist/clients/attach/input-controller.d.ts +4 -3
- package/dist/clients/attach/input-controller.js +24 -3
- package/dist/clients/attach/slash-commands.d.ts +7 -7
- package/dist/clients/attach/slash-commands.js +38 -14
- package/dist/clients/attach/titled-editor.js +18 -14
- package/dist/clients/attach/view-socket.d.ts +2 -1
- package/dist/clients/attach/view-socket.js +27 -8
- package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
- package/dist/clients/web/__tests__/source-cache.test.js +32 -0
- package/dist/clients/web/dev-server.js +1 -0
- package/dist/clients/web/events.js +9 -11
- package/dist/clients/web/server.d.ts +4 -3
- package/dist/clients/web/server.js +138 -31
- package/dist/clients/web/source-cache.d.ts +10 -0
- package/dist/clients/web/source-cache.js +57 -0
- package/dist/clients/web/web-cmd.js +28 -9
- package/dist/commands/__tests__/human.test.js +30 -28
- package/dist/commands/canvas-history/search.js +1 -1
- package/dist/commands/canvas-history.js +5 -8
- package/dist/commands/canvas-issue.d.ts +2 -0
- package/dist/commands/canvas-issue.js +148 -0
- package/dist/commands/canvas-prune.js +19 -0
- package/dist/commands/canvas-rebuild-index.d.ts +2 -0
- package/dist/commands/canvas-rebuild-index.js +57 -0
- package/dist/commands/canvas-snapshot.d.ts +2 -0
- package/dist/commands/canvas-snapshot.js +48 -0
- package/dist/commands/canvas-tmux-spread.d.ts +2 -0
- package/dist/commands/canvas-tmux-spread.js +188 -0
- package/dist/commands/canvas.d.ts +1 -0
- package/dist/commands/canvas.js +16 -1
- package/dist/commands/chord.js +143 -48
- package/dist/commands/daemon.js +3 -3
- package/dist/commands/human/prompts.d.ts +0 -1
- package/dist/commands/human/prompts.js +39 -54
- package/dist/commands/human/queue.d.ts +12 -1
- package/dist/commands/human/queue.js +468 -73
- package/dist/commands/human/shared.d.ts +3 -4
- package/dist/commands/human/shared.js +3 -3
- package/dist/commands/human.js +11 -10
- package/dist/commands/memory/find.js +3 -2
- package/dist/commands/memory/list.js +4 -3
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +1 -4
- package/dist/commands/memory/shared.js +6 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/memory.js +4 -7
- package/dist/commands/node-context.d.ts +2 -0
- package/dist/commands/node-context.js +172 -0
- package/dist/commands/node-snapshot.d.ts +2 -0
- package/dist/commands/node-snapshot.js +123 -0
- package/dist/commands/node.js +228 -52
- package/dist/commands/pkg/plugin-inspect.js +6 -6
- package/dist/commands/pkg/plugin-manage.js +1 -1
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +2 -2
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +79 -11
- package/dist/commands/revive.js +74 -1
- package/dist/commands/search/answer.d.ts +1 -0
- package/dist/commands/search/answer.js +50 -0
- package/dist/commands/search/contents.d.ts +1 -0
- package/dist/commands/search/contents.js +96 -0
- package/dist/commands/search/exa.d.ts +53 -0
- package/dist/commands/search/exa.js +155 -0
- package/dist/commands/search/puremd.d.ts +11 -0
- package/dist/commands/search/puremd.js +53 -0
- package/dist/commands/search/web.d.ts +1 -0
- package/dist/commands/search/web.js +65 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +24 -0
- package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
- package/dist/commands/sys/config.js +59 -10
- package/dist/commands/sys/doctor.js +56 -4
- package/dist/commands/sys/prompt-review.d.ts +1 -0
- package/dist/commands/sys/prompt-review.js +178 -0
- package/dist/commands/sys/promptstudio.d.ts +2 -0
- package/dist/commands/sys/promptstudio.js +65 -0
- package/dist/commands/sys/settings.d.ts +2 -0
- package/dist/commands/sys/settings.js +16 -0
- package/dist/commands/sys/sync.js +311 -159
- package/dist/commands/sys/sysprompt.d.ts +1 -0
- package/dist/commands/sys/sysprompt.js +86 -0
- package/dist/commands/sys.js +9 -5
- package/dist/commands/view-new.js +2 -2
- package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
- package/dist/core/__tests__/artifact-paths.test.js +44 -0
- package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
- package/dist/core/__tests__/broker-preflight.test.js +85 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
- package/dist/core/__tests__/canvas.test.js +37 -1
- package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
- package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
- package/dist/core/__tests__/child-death-wake.test.js +11 -2
- package/dist/core/__tests__/close.test.js +39 -1
- package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
- package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
- package/dist/core/__tests__/context-intro.test.js +48 -14
- package/dist/core/__tests__/daemon-boot.test.js +182 -9
- package/dist/core/__tests__/editor-label.test.d.ts +1 -0
- package/dist/core/__tests__/editor-label.test.js +26 -0
- package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
- package/dist/core/__tests__/fault-marker.test.js +112 -0
- package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
- package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
- package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
- package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +2 -2
- package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
- package/dist/core/__tests__/human-surface-target.test.js +1 -1
- package/dist/core/__tests__/kickoff.test.js +18 -19
- package/dist/core/__tests__/memory-resolver.test.js +12 -1
- package/dist/core/__tests__/migration.test.js +49 -14
- package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
- package/dist/core/__tests__/model-ladders.test.js +160 -0
- package/dist/core/__tests__/node-env.test.d.ts +1 -0
- package/dist/core/__tests__/node-env.test.js +26 -0
- package/dist/core/__tests__/push-final-guard.test.js +7 -1
- package/dist/core/__tests__/relaunch-root.test.js +9 -9
- package/dist/core/__tests__/revive.test.js +36 -6
- package/dist/core/__tests__/spawn-root.test.js +24 -1
- package/dist/core/__tests__/stop-guard.test.js +10 -0
- package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
- package/dist/core/__tests__/tmux-surface.test.js +5 -1
- package/dist/core/__tests__/unknown-path.test.js +19 -19
- package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
- package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
- package/dist/core/artifact.d.ts +2 -33
- package/dist/core/artifact.js +27 -87
- package/dist/core/bootstrap.js +2 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
- package/dist/core/canvas/browse/app.js +87 -17
- package/dist/core/canvas/browse/model.d.ts +10 -1
- package/dist/core/canvas/browse/model.js +37 -1
- package/dist/core/canvas/browse/render.js +35 -33
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +54 -5
- package/dist/core/canvas/db.js +35 -8
- package/dist/core/canvas/labels.d.ts +6 -8
- package/dist/core/canvas/labels.js +8 -11
- package/dist/core/canvas/nav-model.d.ts +5 -4
- package/dist/core/canvas/nav-model.js +31 -17
- package/dist/core/canvas/paths.d.ts +18 -1
- package/dist/core/canvas/paths.js +31 -2
- package/dist/core/canvas/render.d.ts +25 -0
- package/dist/core/canvas/render.js +96 -13
- package/dist/core/canvas/status-glyph.d.ts +35 -0
- package/dist/core/canvas/status-glyph.js +104 -0
- package/dist/core/canvas/types.d.ts +2 -2
- package/dist/core/config.js +114 -16
- package/dist/core/fault-classifier.d.ts +41 -0
- package/dist/core/fault-classifier.js +110 -0
- package/dist/core/feed/inbox.d.ts +15 -0
- package/dist/core/feed/inbox.js +40 -14
- package/dist/core/frontmatter.d.ts +1 -11
- package/dist/core/frontmatter.js +1 -55
- package/dist/core/hearth/config.d.ts +2 -0
- package/dist/core/hearth/config.js +75 -0
- package/dist/core/hearth/guest-env.d.ts +2 -0
- package/dist/core/hearth/guest-env.js +11 -0
- package/dist/core/hearth/index.d.ts +5 -0
- package/dist/core/hearth/index.js +5 -0
- package/dist/core/hearth/provider.d.ts +21 -0
- package/dist/core/hearth/provider.js +10 -0
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
- package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
- package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
- package/dist/core/hearth/providers/blaxel-home.js +126 -0
- package/dist/core/hearth/providers/blaxel.d.ts +26 -0
- package/dist/core/hearth/providers/blaxel.js +208 -0
- package/dist/core/hearth/providers/types.d.ts +72 -0
- package/dist/core/hearth/providers/types.js +1 -0
- package/dist/core/hearth/registry.d.ts +15 -0
- package/dist/core/hearth/registry.js +179 -0
- package/dist/core/hearth/types.d.ts +121 -0
- package/dist/core/hearth/types.js +1 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
- package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
- package/dist/core/host-exports/builtins.js +68 -0
- package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
- package/dist/core/{skill-sync → host-exports}/export.js +37 -14
- package/dist/core/io.js +1 -1
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +113 -0
- package/dist/core/memory-resolver.d.ts +8 -8
- package/dist/core/memory-resolver.js +15 -28
- package/dist/core/personas/index.d.ts +4 -4
- package/dist/core/personas/index.js +2 -2
- package/dist/core/personas/loader.d.ts +51 -0
- package/dist/core/personas/loader.js +54 -50
- package/dist/core/personas/resolve.d.ts +43 -27
- package/dist/core/personas/resolve.js +336 -94
- package/dist/core/runtime/auth-reload.d.ts +7 -0
- package/dist/core/runtime/auth-reload.js +69 -0
- package/dist/core/runtime/bearings.d.ts +35 -15
- package/dist/core/runtime/bearings.js +305 -44
- package/dist/core/runtime/branded-host.d.ts +4 -2
- package/dist/core/runtime/branded-host.js +66 -4
- package/dist/core/runtime/broker-protocol.d.ts +38 -3
- package/dist/core/runtime/broker.js +343 -18
- package/dist/core/runtime/close.js +37 -29
- package/dist/core/runtime/connectivity.d.ts +12 -0
- package/dist/core/runtime/connectivity.js +73 -0
- package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
- package/dist/core/runtime/fault-recovery-nudge.js +2 -0
- package/dist/core/runtime/fault-recovery.d.ts +23 -0
- package/dist/core/runtime/fault-recovery.js +74 -0
- package/dist/core/runtime/fault.d.ts +25 -0
- package/dist/core/runtime/fault.js +176 -0
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/host.d.ts +10 -0
- package/dist/core/runtime/host.js +71 -9
- package/dist/core/runtime/kickoff.js +31 -45
- package/dist/core/runtime/launch.d.ts +32 -18
- package/dist/core/runtime/launch.js +148 -43
- package/dist/core/runtime/model-swap.d.ts +18 -0
- package/dist/core/runtime/model-swap.js +95 -0
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +61 -9
- package/dist/core/runtime/nodes.js +6 -1
- package/dist/core/runtime/persona.js +16 -17
- package/dist/core/runtime/placement.d.ts +36 -31
- package/dist/core/runtime/placement.js +149 -64
- package/dist/core/runtime/promote.d.ts +2 -0
- package/dist/core/runtime/promote.js +23 -3
- package/dist/core/runtime/recycle.js +6 -4
- package/dist/core/runtime/reset.d.ts +5 -2
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.d.ts +4 -0
- package/dist/core/runtime/revive.js +27 -9
- package/dist/core/runtime/roadmap.d.ts +1 -1
- package/dist/core/runtime/roadmap.js +1 -1
- package/dist/core/runtime/spawn.d.ts +2 -2
- package/dist/core/runtime/spawn.js +30 -6
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +29 -6
- package/dist/core/runtime/tmux.js +134 -80
- package/dist/core/runtime/view-socket-client.d.ts +46 -0
- package/dist/core/runtime/view-socket-client.js +203 -0
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/on-read.js +16 -4
- package/dist/core/substrate/render.d.ts +10 -13
- package/dist/core/substrate/render.js +56 -51
- package/dist/core/tui/host.js +83 -18
- package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
- package/dist/core/view/__tests__/transport-cache.test.js +62 -0
- package/dist/core/view/contract.d.ts +20 -0
- package/dist/core/view/stream-local.d.ts +3 -0
- package/dist/core/view/stream-local.js +184 -0
- package/dist/core/view/transport-cache.d.ts +8 -0
- package/dist/core/view/transport-cache.js +38 -0
- package/dist/core/view/transport-local.js +4 -0
- package/dist/core/view/transport.d.ts +7 -1
- package/dist/daemon/crtrd.d.ts +3 -19
- package/dist/daemon/crtrd.js +139 -178
- package/dist/daemon/manage.d.ts +18 -1
- package/dist/daemon/manage.js +54 -9
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
- package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
- package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
- package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
- package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
- package/dist/hearth/wake-proxy/auth.d.ts +22 -0
- package/dist/hearth/wake-proxy/auth.js +128 -0
- package/dist/hearth/wake-proxy/config.d.ts +2 -0
- package/dist/hearth/wake-proxy/config.js +151 -0
- package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
- package/dist/hearth/wake-proxy/guest-source.js +130 -0
- package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
- package/dist/hearth/wake-proxy/hearth-status.js +267 -0
- package/dist/hearth/wake-proxy/home.d.ts +67 -0
- package/dist/hearth/wake-proxy/home.js +297 -0
- package/dist/hearth/wake-proxy/main.d.ts +1 -0
- package/dist/hearth/wake-proxy/main.js +134 -0
- package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
- package/dist/hearth/wake-proxy/model-auth.js +345 -0
- package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
- package/dist/hearth/wake-proxy/proxy.js +716 -0
- package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
- package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
- package/dist/hearth/wake-proxy/redact.d.ts +1 -0
- package/dist/hearth/wake-proxy/redact.js +17 -0
- package/dist/hearth/wake-proxy/server.d.ts +11 -0
- package/dist/hearth/wake-proxy/server.js +142 -0
- package/dist/hearth/wake-proxy/state.d.ts +18 -0
- package/dist/hearth/wake-proxy/state.js +342 -0
- package/dist/hearth/wake-proxy/types.d.ts +76 -0
- package/dist/hearth/wake-proxy/types.js +1 -0
- package/dist/index.js +3 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
- package/dist/pi-extensions/canvas-context-intro.js +7 -7
- package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
- package/dist/pi-extensions/canvas-nav.js +15 -8
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +78 -11
- package/dist/types.d.ts +39 -20
- package/dist/types.js +38 -13
- package/dist/web/runtime.js +141 -42
- package/dist/web/transport-http.js +7 -5
- package/dist/web/transport-stream.d.ts +3 -0
- package/dist/web/transport-stream.js +204 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
- package/dist/web-client/assets/index-D36PNBj4.js +80 -0
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
- package/dist/web-client/index.html +3 -2
- package/package.json +32 -10
- package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
- package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
- package/dist/core/skill-sync/builtins.js +0 -112
- package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
- package/dist/core/skill-sync/claude-plugins.js +0 -71
- package/dist/core/skill-sync/engine.d.ts +0 -42
- package/dist/core/skill-sync/engine.js +0 -633
- package/dist/core/skill-sync/manifest.d.ts +0 -64
- package/dist/core/skill-sync/manifest.js +0 -181
- package/dist/core/skill-sync/profile.d.ts +0 -76
- package/dist/core/skill-sync/profile.js +0 -173
- package/dist/core/skill-sync/snapshot.d.ts +0 -57
- package/dist/core/skill-sync/snapshot.js +0 -120
- package/dist/pi-extensions/canvas-commands.d.ts +0 -37
- package/dist/pi-extensions/canvas-commands.js +0 -113
- package/dist/pi-extensions/canvas-resume.d.ts +0 -21
- package/dist/pi-extensions/canvas-resume.js +0 -83
- package/dist/pi-extensions/canvas-view.d.ts +0 -21
- package/dist/pi-extensions/canvas-view.js +0 -76
- package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
- package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
- /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
- /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { linkSync, mkdtempSync, mkdirSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { PUBLIC_SOURCE_FILE_READ_SCRIPT, handlePublicSourceRequest } from '../public-source-gate.js';
|
|
10
|
+
import { guestEnv } from '../../../core/hearth/guest-env.js';
|
|
11
|
+
import { routeRequest } from '../server.js';
|
|
12
|
+
const ownerToken = 'owner-secret-token';
|
|
13
|
+
const ownerTokenHash = createHash('sha256').update(ownerToken, 'utf8').digest('hex');
|
|
14
|
+
const publicOrigin = 'https://hearth.example';
|
|
15
|
+
const guestHome = '/home/agent';
|
|
16
|
+
const tenantId = 'tenant-source-gate';
|
|
17
|
+
const nodeId = 'mqt64sus-e71caa94';
|
|
18
|
+
function makeRuntime(statePath) {
|
|
19
|
+
return {
|
|
20
|
+
proxy: {
|
|
21
|
+
host: '127.0.0.1',
|
|
22
|
+
port: 7878,
|
|
23
|
+
publicOrigin,
|
|
24
|
+
statePath,
|
|
25
|
+
m0RegistryPath: join(statePath, 'homes.json'),
|
|
26
|
+
ownerUserId: 'owner-user',
|
|
27
|
+
defaultTenantId: tenantId,
|
|
28
|
+
ownerTokenHash,
|
|
29
|
+
idlePauseMs: 60_000,
|
|
30
|
+
wakeTimeoutMs: 30_000,
|
|
31
|
+
readyTimeoutMs: 2_000,
|
|
32
|
+
cookieSecure: true,
|
|
33
|
+
seedHome: null,
|
|
34
|
+
},
|
|
35
|
+
m0: {
|
|
36
|
+
providerName: 'none',
|
|
37
|
+
registryPath: join(statePath, 'homes.json'),
|
|
38
|
+
guestUser: 'agent',
|
|
39
|
+
guestHome,
|
|
40
|
+
guestCodePath: '/opt/crouter',
|
|
41
|
+
webPort: 7878,
|
|
42
|
+
nodeKind: 'general',
|
|
43
|
+
piPackages: [],
|
|
44
|
+
piAuthJson: null,
|
|
45
|
+
relayToken: null,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function writeState(statePath) {
|
|
50
|
+
writeFileSync(statePath, `${JSON.stringify({
|
|
51
|
+
version: 1,
|
|
52
|
+
owner: {
|
|
53
|
+
ownerUserId: 'owner-user',
|
|
54
|
+
defaultTenantId: tenantId,
|
|
55
|
+
tokenHash: ownerTokenHash,
|
|
56
|
+
createdAt: '2026-06-26T00:00:00.000Z',
|
|
57
|
+
},
|
|
58
|
+
tenants: {},
|
|
59
|
+
}, null, 2)}\n`, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
function makeRequest(method, url, headers = {}, body = '') {
|
|
62
|
+
const req = new Readable({ read() { } });
|
|
63
|
+
req.method = method;
|
|
64
|
+
req.url = url;
|
|
65
|
+
req.headers = headers;
|
|
66
|
+
process.nextTick(() => {
|
|
67
|
+
if (body !== '')
|
|
68
|
+
req.push(body);
|
|
69
|
+
req.push(null);
|
|
70
|
+
});
|
|
71
|
+
return req;
|
|
72
|
+
}
|
|
73
|
+
function makeResponse() {
|
|
74
|
+
return {
|
|
75
|
+
statusCode: 200,
|
|
76
|
+
headersSent: false,
|
|
77
|
+
headers: {},
|
|
78
|
+
body: '',
|
|
79
|
+
writeHead(statusCode, headers) {
|
|
80
|
+
this.statusCode = statusCode;
|
|
81
|
+
this.headers = { ...this.headers, ...headers };
|
|
82
|
+
this.headersSent = true;
|
|
83
|
+
return this;
|
|
84
|
+
},
|
|
85
|
+
setHeader(name, value) {
|
|
86
|
+
this.headers[name.toLowerCase()] = value;
|
|
87
|
+
},
|
|
88
|
+
end(chunk) {
|
|
89
|
+
if (chunk !== undefined)
|
|
90
|
+
this.body += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
|
|
91
|
+
this.headersSent = true;
|
|
92
|
+
return this;
|
|
93
|
+
},
|
|
94
|
+
destroy() {
|
|
95
|
+
this.headersSent = true;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseBody(res) {
|
|
100
|
+
return JSON.parse(res.body);
|
|
101
|
+
}
|
|
102
|
+
function helperResult(input) {
|
|
103
|
+
const parsed = JSON.parse(String(input));
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
function runArtifactHelper(canvasHomePath, piAgentDirPath, artifactPath) {
|
|
107
|
+
const proc = spawnSync(process.execPath, ['--input-type=module', '-e', PUBLIC_SOURCE_FILE_READ_SCRIPT], {
|
|
108
|
+
input: JSON.stringify({ canvasHome: canvasHomePath, piAgentDir: piAgentDirPath, path: artifactPath }),
|
|
109
|
+
encoding: 'utf8',
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
status: proc.status,
|
|
113
|
+
stdout: proc.stdout,
|
|
114
|
+
stderr: proc.stderr,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
test('regression: public source gate allows stdin only on crtr node new/msg writes (fixes MAJOR 1)', async () => {
|
|
118
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-gate-'));
|
|
119
|
+
const statePath = join(tempDir, 'state.json');
|
|
120
|
+
writeState(statePath);
|
|
121
|
+
const runtime = makeRuntime(statePath);
|
|
122
|
+
const calls = [];
|
|
123
|
+
const allowedExecCases = [
|
|
124
|
+
{ kind: 'exec', bin: 'crtr', args: ['canvas', 'snapshot', '--json'] },
|
|
125
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'show', nodeId, '--json'] },
|
|
126
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'snapshot', nodeId, '--json'] },
|
|
127
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'new', '--kind', 'developer', '--cwd', '/home/agent/project', '--name', 'spawned-child', '--model', 'claude-sonnet-4', '--parent', nodeId, '--json'], stdin: 'spawn prompt body' },
|
|
128
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'msg', nodeId, '--json'], stdin: 'ping the node body' },
|
|
129
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'msg', nodeId, '--tier', 'urgent', '--json'], stdin: 'ping the node with tier' },
|
|
130
|
+
{ kind: 'exec', bin: 'crtr', args: ['canvas', 'revive', nodeId, '--json'] },
|
|
131
|
+
{ kind: 'exec', bin: 'crtr', args: ['canvas', 'revive', nodeId, '--fresh', '--json'] },
|
|
132
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'close', '--node', nodeId, '--json'] },
|
|
133
|
+
{ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] },
|
|
134
|
+
];
|
|
135
|
+
const guestSource = async (_tenantId, request) => {
|
|
136
|
+
calls.push({ tenantId: _tenantId, request });
|
|
137
|
+
const typed = request;
|
|
138
|
+
if (typed.kind === 'exec' && typed.bin === 'node' && Array.isArray(typed.args) && typed.args.includes(PUBLIC_SOURCE_FILE_READ_SCRIPT)) {
|
|
139
|
+
const payload = JSON.parse(typed.stdin ?? '{}');
|
|
140
|
+
const fileContent = payload.path?.endsWith('/reports/report.md') ? 'reports artifact contents' : 'context artifact contents';
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
exitCode: 0,
|
|
144
|
+
stdout: JSON.stringify({ ok: true, stdout: fileContent }),
|
|
145
|
+
stderr: '',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
exitCode: 0,
|
|
151
|
+
stdout: 'allowed exec ok',
|
|
152
|
+
stderr: '',
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
for (const request of allowedExecCases) {
|
|
156
|
+
const res = makeResponse();
|
|
157
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', { authorization: `Bearer ${ownerToken}` }, JSON.stringify(request)), res, runtime, { guestSource });
|
|
158
|
+
assert.equal(handled, true);
|
|
159
|
+
assert.equal(res.statusCode, 200);
|
|
160
|
+
assert.deepEqual(parseBody(res), {
|
|
161
|
+
ok: true,
|
|
162
|
+
exitCode: 0,
|
|
163
|
+
stdout: 'allowed exec ok',
|
|
164
|
+
stderr: '',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const env = guestEnv(runtime.m0);
|
|
168
|
+
const contextPath = `${env.CRTR_HOME}/nodes/${nodeId}/context/note.md`;
|
|
169
|
+
const reportsPath = `${env.CRTR_HOME}/nodes/${nodeId}/reports/report.md`;
|
|
170
|
+
for (const filePath of [contextPath, reportsPath]) {
|
|
171
|
+
const res = makeResponse();
|
|
172
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', { authorization: `Bearer ${ownerToken}` }, JSON.stringify({ kind: 'file', path: filePath })), res, runtime, { guestSource });
|
|
173
|
+
assert.equal(handled, true);
|
|
174
|
+
assert.equal(res.statusCode, 200);
|
|
175
|
+
assert.deepEqual(parseBody(res), {
|
|
176
|
+
ok: true,
|
|
177
|
+
stdout: filePath.endsWith('/reports/report.md') ? 'reports artifact contents' : 'context artifact contents',
|
|
178
|
+
stderr: '',
|
|
179
|
+
exitCode: 0,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const helperCall = calls.find((call) => {
|
|
183
|
+
const request = call.request;
|
|
184
|
+
return request.kind === 'exec' && request.bin === 'node' && Array.isArray(request.args) && request.args.includes(PUBLIC_SOURCE_FILE_READ_SCRIPT);
|
|
185
|
+
});
|
|
186
|
+
assert.ok(helperCall);
|
|
187
|
+
const helperRequest = helperCall?.request;
|
|
188
|
+
assert.ok(helperRequest.stdin);
|
|
189
|
+
const helperPayload = JSON.parse(helperRequest.stdin ?? '{}');
|
|
190
|
+
assert.equal(helperPayload.canvasHome, env.CRTR_HOME);
|
|
191
|
+
assert.equal(helperPayload.piAgentDir, env.PI_CODING_AGENT_DIR);
|
|
192
|
+
assert.ok(helperPayload.path?.startsWith(`${env.CRTR_HOME}/nodes/${nodeId}/`));
|
|
193
|
+
const invalidRequests = [
|
|
194
|
+
{ kind: 'exec', bin: 'node', args: ['-v'], stdin: 'browser-supplied stdin' },
|
|
195
|
+
{ kind: 'exec', bin: 'sh', args: ['-c', 'pwd'] },
|
|
196
|
+
{ kind: 'exec', bin: 'git', args: ['status', '--json'], stdin: 'browser-supplied stdin' },
|
|
197
|
+
{ kind: 'exec', bin: 'gh', args: ['issue', 'list', '--json'] },
|
|
198
|
+
{ kind: 'exec', bin: 'capture', args: ['shot', '--json'] },
|
|
199
|
+
{ kind: 'exec', bin: 'tmux', args: ['list-sessions', '--json'] },
|
|
200
|
+
{ kind: 'exec', bin: 'pwd', args: ['--json'] },
|
|
201
|
+
{ kind: 'exec', bin: 'crtr', args: ['canvas', 'status', '--json'] },
|
|
202
|
+
{ kind: 'exec', bin: 'crtr', args: ['canvas', 'snapshot', '--extra', '--json'] },
|
|
203
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'inspect', 'show', nodeId, '--extra', '--json'] },
|
|
204
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'new', '--kind', 'developer', '--json'], stdin: 'x'.repeat(16 * 1024 + 1) },
|
|
205
|
+
{ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--extra', '--json'] },
|
|
206
|
+
{ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'], cwd: '/tmp' },
|
|
207
|
+
{ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'], stdin: 'browser-supplied stdin' },
|
|
208
|
+
{ kind: 'exec', bin: 'crtr', args: ['node', 'close', '--node', nodeId] },
|
|
209
|
+
{ kind: 'http', method: 'GET', url: 'https://example.invalid' },
|
|
210
|
+
{ kind: 'file', path: `${env.PI_CODING_AGENT_DIR}/auth.json` },
|
|
211
|
+
{ kind: 'file', path: '/etc/passwd' },
|
|
212
|
+
{ kind: 'file', path: `${env.CRTR_HOME}/nodes/${nodeId}/context/../reports/report.md` },
|
|
213
|
+
{ kind: 'file', path: `${env.CRTR_HOME}/nodes/${nodeId}/secret/x.md` },
|
|
214
|
+
{ kind: 'file', path: `${env.CRTR_HOME}\\nodes\\${nodeId}\\context\\note.md` },
|
|
215
|
+
];
|
|
216
|
+
for (const request of invalidRequests) {
|
|
217
|
+
const res = makeResponse();
|
|
218
|
+
let called = 0;
|
|
219
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', { authorization: `Bearer ${ownerToken}` }, JSON.stringify(request)), res, runtime, {
|
|
220
|
+
guestSource: async () => {
|
|
221
|
+
called += 1;
|
|
222
|
+
throw new Error('guest should not run for rejected source requests');
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
assert.equal(handled, true);
|
|
226
|
+
assert.equal(called, 0);
|
|
227
|
+
assert.equal(res.statusCode, 403);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
test('routeRequest dispatches source and model-auth before proxy fallback', async () => {
|
|
231
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-route-'));
|
|
232
|
+
const statePath = join(tempDir, 'state.json');
|
|
233
|
+
writeState(statePath);
|
|
234
|
+
const runtime = makeRuntime(statePath);
|
|
235
|
+
let sourceCalls = 0;
|
|
236
|
+
const sourceRes = makeResponse();
|
|
237
|
+
await routeRequest(makeRequest('POST', '/__crtr/source', { authorization: `Bearer ${ownerToken}` }, JSON.stringify({ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] })), sourceRes, runtime, {
|
|
238
|
+
sourceGate: {
|
|
239
|
+
guestSource: async () => {
|
|
240
|
+
sourceCalls += 1;
|
|
241
|
+
return { ok: true, exitCode: 0, stdout: 'source routed', stderr: '' };
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
assert.equal(sourceCalls, 1);
|
|
246
|
+
assert.equal(sourceRes.statusCode, 200);
|
|
247
|
+
assert.deepEqual(parseBody(sourceRes), {
|
|
248
|
+
ok: true,
|
|
249
|
+
exitCode: 0,
|
|
250
|
+
stdout: 'source routed',
|
|
251
|
+
stderr: ''
|
|
252
|
+
});
|
|
253
|
+
let modelCalls = 0;
|
|
254
|
+
const modelRes = makeResponse();
|
|
255
|
+
await routeRequest(makeRequest('GET', '/__hearth/model-auth/anthropic/status', { authorization: `Bearer ${ownerToken}` }), modelRes, runtime, {
|
|
256
|
+
modelAuth: {
|
|
257
|
+
guestSource: async () => {
|
|
258
|
+
modelCalls += 1;
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
exitCode: 0,
|
|
262
|
+
stdout: JSON.stringify({ storedOauth: false, expiresAt: null, adapterConfigured: true }),
|
|
263
|
+
stderr: '',
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
assert.equal(modelCalls, 1);
|
|
269
|
+
assert.equal(modelRes.statusCode, 200);
|
|
270
|
+
assert.deepEqual(parseBody(modelRes), {
|
|
271
|
+
storedOauth: false,
|
|
272
|
+
expiresAt: null,
|
|
273
|
+
adapterConfigured: true,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
test('public source gate enforces owner write auth and leaves non-source chat routes alone', async () => {
|
|
277
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-auth-'));
|
|
278
|
+
const statePath = join(tempDir, 'state.json');
|
|
279
|
+
writeState(statePath);
|
|
280
|
+
const runtime = makeRuntime(statePath);
|
|
281
|
+
let rejectedGuestCalls = 0;
|
|
282
|
+
const rejectingGuestSource = async () => {
|
|
283
|
+
rejectedGuestCalls += 1;
|
|
284
|
+
return { ok: true, exitCode: 0, stdout: 'ok', stderr: '' };
|
|
285
|
+
};
|
|
286
|
+
{
|
|
287
|
+
const res = makeResponse();
|
|
288
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source?token=' + encodeURIComponent(ownerToken), {}, JSON.stringify({ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] })), res, runtime, { guestSource: rejectingGuestSource });
|
|
289
|
+
assert.equal(handled, true);
|
|
290
|
+
assert.equal(rejectedGuestCalls, 0);
|
|
291
|
+
assert.equal(res.statusCode, 401);
|
|
292
|
+
}
|
|
293
|
+
{
|
|
294
|
+
rejectedGuestCalls = 0;
|
|
295
|
+
const res = makeResponse();
|
|
296
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', {
|
|
297
|
+
cookie: `hearth_owner=${encodeURIComponent(ownerToken)}`,
|
|
298
|
+
}, JSON.stringify({ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] })), res, runtime, { guestSource: rejectingGuestSource });
|
|
299
|
+
assert.equal(handled, true);
|
|
300
|
+
assert.equal(rejectedGuestCalls, 0);
|
|
301
|
+
assert.equal(res.statusCode, 403);
|
|
302
|
+
}
|
|
303
|
+
{
|
|
304
|
+
const res = makeResponse();
|
|
305
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', {
|
|
306
|
+
cookie: `hearth_owner=${encodeURIComponent(ownerToken)}`,
|
|
307
|
+
origin: publicOrigin,
|
|
308
|
+
}, JSON.stringify({ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] })), res, runtime, { guestSource: async () => ({ ok: true, exitCode: 0, stdout: 'ok', stderr: '' }) });
|
|
309
|
+
assert.equal(handled, true);
|
|
310
|
+
assert.equal(res.statusCode, 200);
|
|
311
|
+
}
|
|
312
|
+
{
|
|
313
|
+
const res = makeResponse();
|
|
314
|
+
const handled = await handlePublicSourceRequest(makeRequest('POST', '/__crtr/source', {
|
|
315
|
+
authorization: `Bearer ${ownerToken}`,
|
|
316
|
+
}, JSON.stringify({ kind: 'exec', bin: 'crtr', args: ['view', 'list', '--json'] })), res, runtime, { guestSource: async () => ({ ok: true, exitCode: 0, stdout: 'ok', stderr: '' }) });
|
|
317
|
+
assert.equal(handled, true);
|
|
318
|
+
assert.equal(res.statusCode, 200);
|
|
319
|
+
}
|
|
320
|
+
{
|
|
321
|
+
const res = makeResponse();
|
|
322
|
+
const handled = await handlePublicSourceRequest(makeRequest('GET', '/node/' + nodeId, { authorization: `Bearer ${ownerToken}` }), res, runtime, { guestSource: async () => ({ ok: true, exitCode: 0, stdout: 'ok', stderr: '' }) });
|
|
323
|
+
assert.equal(handled, false);
|
|
324
|
+
assert.equal(res.headersSent, false);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
test('the guest artifact helper enforces realpath containment before reading', () => {
|
|
328
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-helper-'));
|
|
329
|
+
const canvasHomePath = join(tempDir, '.crtr-local', 'canvas');
|
|
330
|
+
const piAgentDirPath = join(tempDir, '.crtr-local', 'pi', 'agent');
|
|
331
|
+
mkdirSync(canvasHomePath, { recursive: true });
|
|
332
|
+
mkdirSync(piAgentDirPath, { recursive: true });
|
|
333
|
+
const canonicalCanvasHomePath = realpathSync(canvasHomePath);
|
|
334
|
+
const canonicalPiAgentDirPath = realpathSync(piAgentDirPath);
|
|
335
|
+
const contextDir = join(canonicalCanvasHomePath, 'nodes', nodeId, 'context');
|
|
336
|
+
const reportsDir = join(canonicalCanvasHomePath, 'nodes', nodeId, 'reports');
|
|
337
|
+
mkdirSync(contextDir, { recursive: true });
|
|
338
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
339
|
+
const contextFile = join(contextDir, 'note.md');
|
|
340
|
+
const reportsFile = join(reportsDir, 'report.md');
|
|
341
|
+
writeFileSync(contextFile, 'context artifact contents', 'utf8');
|
|
342
|
+
writeFileSync(reportsFile, 'reports artifact contents', 'utf8');
|
|
343
|
+
const outsideFile = join(tempDir, 'outside.txt');
|
|
344
|
+
writeFileSync(outsideFile, 'sensitive contents', 'utf8');
|
|
345
|
+
const leakLink = join(contextDir, 'leak.md');
|
|
346
|
+
symlinkSync(outsideFile, leakLink);
|
|
347
|
+
const contextRun = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, contextFile);
|
|
348
|
+
assert.equal(contextRun.status, 0);
|
|
349
|
+
const contextResult = helperResult(contextRun.stdout.trim());
|
|
350
|
+
assert.equal(contextResult.ok, true);
|
|
351
|
+
assert.equal(contextResult.stdout, 'context artifact contents');
|
|
352
|
+
const reportsRun = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, reportsFile);
|
|
353
|
+
assert.equal(reportsRun.status, 0);
|
|
354
|
+
const reportsResult = helperResult(reportsRun.stdout.trim());
|
|
355
|
+
assert.equal(reportsResult.ok, true);
|
|
356
|
+
assert.equal(reportsResult.stdout, 'reports artifact contents');
|
|
357
|
+
const mixedSeparatorPath = `${canonicalCanvasHomePath}/nodes\\${nodeId}\\context\\note.md`;
|
|
358
|
+
const mixedSeparatorRun = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, mixedSeparatorPath);
|
|
359
|
+
assert.equal(mixedSeparatorRun.status, 0);
|
|
360
|
+
const mixedSeparatorResult = helperResult(mixedSeparatorRun.stdout.trim());
|
|
361
|
+
assert.equal(mixedSeparatorResult.ok, false);
|
|
362
|
+
assert.equal(mixedSeparatorResult.status, 403);
|
|
363
|
+
const leakRun = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, leakLink);
|
|
364
|
+
assert.equal(leakRun.status, 0);
|
|
365
|
+
const leakResult = helperResult(leakRun.stdout.trim());
|
|
366
|
+
assert.equal(leakResult.ok, false);
|
|
367
|
+
assert.equal(leakResult.status, 403);
|
|
368
|
+
assert.ok(!leakResult.error?.includes('sensitive'));
|
|
369
|
+
});
|
|
370
|
+
test('the guest artifact helper rejects hard-linked aliases to secrets under an allowed artifact root', () => {
|
|
371
|
+
// Regression for the hard-link alias bypass found in the public source gate.
|
|
372
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-helper-hardlink-'));
|
|
373
|
+
const canvasHomePath = join(tempDir, '.crtr-local', 'canvas');
|
|
374
|
+
const piAgentDirPath = join(tempDir, '.crtr-local', 'pi', 'agent');
|
|
375
|
+
mkdirSync(canvasHomePath, { recursive: true });
|
|
376
|
+
mkdirSync(piAgentDirPath, { recursive: true });
|
|
377
|
+
const canonicalCanvasHomePath = realpathSync(canvasHomePath);
|
|
378
|
+
const canonicalPiAgentDirPath = realpathSync(piAgentDirPath);
|
|
379
|
+
const contextDir = join(canonicalCanvasHomePath, 'nodes', nodeId, 'context');
|
|
380
|
+
mkdirSync(contextDir, { recursive: true });
|
|
381
|
+
const secretPath = join(piAgentDirPath, 'auth.json');
|
|
382
|
+
writeFileSync(secretPath, 'super-secret-token', 'utf8');
|
|
383
|
+
const hardLinkPath = join(contextDir, 'auth.json');
|
|
384
|
+
linkSync(secretPath, hardLinkPath);
|
|
385
|
+
const run = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, hardLinkPath);
|
|
386
|
+
assert.equal(run.status, 0);
|
|
387
|
+
const result = helperResult(run.stdout.trim());
|
|
388
|
+
assert.equal(result.ok, false);
|
|
389
|
+
assert.equal(result.status, 403);
|
|
390
|
+
assert.ok(!result.error?.includes('super-secret-token'));
|
|
391
|
+
});
|
|
392
|
+
test('the guest artifact helper rejects a symlinked nodesBase root', () => {
|
|
393
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-helper-nodes-base-'));
|
|
394
|
+
const canvasHomePath = join(tempDir, '.crtr-local', 'canvas');
|
|
395
|
+
const piAgentDirPath = join(tempDir, '.crtr-local', 'pi', 'agent');
|
|
396
|
+
mkdirSync(canvasHomePath, { recursive: true });
|
|
397
|
+
mkdirSync(piAgentDirPath, { recursive: true });
|
|
398
|
+
const canonicalCanvasHomePath = realpathSync(canvasHomePath);
|
|
399
|
+
const canonicalPiAgentDirPath = realpathSync(piAgentDirPath);
|
|
400
|
+
const nodesDir = join(canonicalCanvasHomePath, 'nodes');
|
|
401
|
+
mkdirSync(canonicalCanvasHomePath, { recursive: true });
|
|
402
|
+
const outsideNodes = join(tempDir, 'outside-nodes');
|
|
403
|
+
const canonicalNodeId = 'evil-node';
|
|
404
|
+
mkdirSync(join(outsideNodes, canonicalNodeId, 'context'), { recursive: true });
|
|
405
|
+
writeFileSync(join(outsideNodes, canonicalNodeId, 'context', 'note.md'), 'sensitive nodesBase contents', 'utf8');
|
|
406
|
+
symlinkSync(outsideNodes, nodesDir);
|
|
407
|
+
const run = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, join(nodesDir, canonicalNodeId, 'context', 'note.md'));
|
|
408
|
+
assert.equal(run.status, 0);
|
|
409
|
+
const result = helperResult(run.stdout.trim());
|
|
410
|
+
assert.equal(result.ok, false);
|
|
411
|
+
assert.equal(result.status, 403);
|
|
412
|
+
assert.ok(!result.error?.includes('sensitive'));
|
|
413
|
+
});
|
|
414
|
+
test('the guest artifact helper rejects symlinked roots, ancestor subdirectories, and symlinked node directories that escape the canonical nodes tree', () => {
|
|
415
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-source-helper-symlink-'));
|
|
416
|
+
const canvasHomePath = join(tempDir, '.crtr-local', 'canvas');
|
|
417
|
+
const piAgentDirPath = join(tempDir, '.crtr-local', 'pi', 'agent');
|
|
418
|
+
mkdirSync(canvasHomePath, { recursive: true });
|
|
419
|
+
mkdirSync(piAgentDirPath, { recursive: true });
|
|
420
|
+
const canonicalCanvasHomePath = realpathSync(canvasHomePath);
|
|
421
|
+
const canonicalPiAgentDirPath = realpathSync(piAgentDirPath);
|
|
422
|
+
const nodesDir = join(canonicalCanvasHomePath, 'nodes');
|
|
423
|
+
const canonicalNodeId = 'evil-node';
|
|
424
|
+
const canonicalNodeDir = join(nodesDir, canonicalNodeId);
|
|
425
|
+
mkdirSync(canonicalNodeDir, { recursive: true });
|
|
426
|
+
const assertBlockedArtifact = (artifactPath) => {
|
|
427
|
+
const run = runArtifactHelper(canonicalCanvasHomePath, canonicalPiAgentDirPath, artifactPath);
|
|
428
|
+
assert.equal(run.status, 0);
|
|
429
|
+
const result = helperResult(run.stdout.trim());
|
|
430
|
+
assert.equal(result.ok, false);
|
|
431
|
+
assert.equal(result.status, 403);
|
|
432
|
+
assert.ok(!result.error?.includes('sensitive'));
|
|
433
|
+
};
|
|
434
|
+
const contextOutside = join(tempDir, 'outside-context');
|
|
435
|
+
mkdirSync(contextOutside, { recursive: true });
|
|
436
|
+
writeFileSync(join(contextOutside, 'note.md'), 'sensitive context contents', 'utf8');
|
|
437
|
+
symlinkSync(contextOutside, join(canonicalNodeDir, 'context'));
|
|
438
|
+
assertBlockedArtifact(join(canonicalNodeDir, 'context', 'note.md'));
|
|
439
|
+
const contextSubOutside = join(tempDir, 'outside-context-sub');
|
|
440
|
+
mkdirSync(contextSubOutside, { recursive: true });
|
|
441
|
+
writeFileSync(join(contextSubOutside, 'file'), 'sensitive ancestor subdir contents', 'utf8');
|
|
442
|
+
symlinkSync(contextSubOutside, join(canonicalNodeDir, 'context', 'sub'));
|
|
443
|
+
assertBlockedArtifact(join(canonicalNodeDir, 'context', 'sub', 'file'));
|
|
444
|
+
const reportsOutside = join(tempDir, 'outside-reports');
|
|
445
|
+
mkdirSync(reportsOutside, { recursive: true });
|
|
446
|
+
writeFileSync(join(reportsOutside, 'report.md'), 'sensitive reports contents', 'utf8');
|
|
447
|
+
symlinkSync(reportsOutside, join(canonicalNodeDir, 'reports'));
|
|
448
|
+
assertBlockedArtifact(join(canonicalNodeDir, 'reports', 'report.md'));
|
|
449
|
+
const symlinkedNodeId = 'evil-node-link';
|
|
450
|
+
const outsideNodeDir = join(tempDir, 'outside-node');
|
|
451
|
+
mkdirSync(join(outsideNodeDir, 'context'), { recursive: true });
|
|
452
|
+
writeFileSync(join(outsideNodeDir, 'context', 'note.md'), 'sensitive node contents', 'utf8');
|
|
453
|
+
symlinkSync(outsideNodeDir, join(nodesDir, symlinkedNodeId));
|
|
454
|
+
assertBlockedArtifact(join(nodesDir, symlinkedNodeId, 'context', 'note.md'));
|
|
455
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { isPublicStaticAssetPath } from '../proxy.js';
|
|
4
|
+
test('wake proxy treats SPA bundle and shell assets as public', () => {
|
|
5
|
+
assert.equal(isPublicStaticAssetPath('/assets/index-1234abcd.js'), true);
|
|
6
|
+
assert.equal(isPublicStaticAssetPath('/assets/style-1234abcd.css'), true);
|
|
7
|
+
assert.equal(isPublicStaticAssetPath('/favicon.ico'), true);
|
|
8
|
+
assert.equal(isPublicStaticAssetPath('/vite.svg'), true);
|
|
9
|
+
});
|
|
10
|
+
test('wake proxy keeps app routes behind auth', () => {
|
|
11
|
+
assert.equal(isPublicStaticAssetPath('/'), false);
|
|
12
|
+
assert.equal(isPublicStaticAssetPath('/views/chat'), false);
|
|
13
|
+
assert.equal(isPublicStaticAssetPath('/__crtr/events'), false);
|
|
14
|
+
assert.equal(isPublicStaticAssetPath('/node/mqt64sus-e71caa94'), false);
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { ensureAwake, configureWakeProxyRuntime, wakeProxyInternals } from '../home.js';
|
|
7
|
+
// Regression for the live soak stall (REPORT.md): every HTTP forward and WS connect funneled through
|
|
8
|
+
// the per-tenant serial wake lock, so a reconnect/SSE/asset burst queued behind each other for up to
|
|
9
|
+
// the 150s wake timeout while the home was directly healthy (<0.4s). The warm read path must NOT run
|
|
10
|
+
// N serial slow probes and must NOT wait behind a stale slow wake when a fresh warm verdict exists.
|
|
11
|
+
const tenantId = 'silas';
|
|
12
|
+
const machineId = 'igd2fkrn1y1ol3vw5i6mt';
|
|
13
|
+
const nodeId = 'mqt64sus-e71caa94';
|
|
14
|
+
const routeUrl = 'https://7878-igd2fkrn1y1ol3vw5i6mt.example.dev';
|
|
15
|
+
const now = '2026-06-25T00:00:00.000Z';
|
|
16
|
+
function makeHome(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
tenantId,
|
|
19
|
+
providerName: 'none',
|
|
20
|
+
machineId,
|
|
21
|
+
homeStorage: {
|
|
22
|
+
kind: 'volume',
|
|
23
|
+
durability: 'durable-volume',
|
|
24
|
+
longTermContract: true,
|
|
25
|
+
ownerMachineId: machineId,
|
|
26
|
+
guestHome: '/home/agent',
|
|
27
|
+
volumeId: '48b59d07-8cc9-46f9-bdfd-ac99a55dff40',
|
|
28
|
+
mountPath: '/home/agent',
|
|
29
|
+
},
|
|
30
|
+
state: 'running',
|
|
31
|
+
nodeId,
|
|
32
|
+
relayToken: 'relay-token',
|
|
33
|
+
webRoute: {
|
|
34
|
+
routeId: '7878-igd2fkrn1y1ol3vw5i6mt.example.dev',
|
|
35
|
+
machineId,
|
|
36
|
+
targetPort: 7878,
|
|
37
|
+
url: routeUrl,
|
|
38
|
+
concreteUrl: routeUrl,
|
|
39
|
+
concreteUrlSource: 'provider',
|
|
40
|
+
createdAt: now,
|
|
41
|
+
raw: null,
|
|
42
|
+
},
|
|
43
|
+
createdAt: now,
|
|
44
|
+
updatedAt: now,
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function makeRuntime(registryPath, statePath) {
|
|
49
|
+
return {
|
|
50
|
+
proxy: {
|
|
51
|
+
host: '127.0.0.1',
|
|
52
|
+
port: 7878,
|
|
53
|
+
publicOrigin: 'https://hearth-wake-proxy.fly.dev',
|
|
54
|
+
statePath,
|
|
55
|
+
m0RegistryPath: registryPath,
|
|
56
|
+
ownerUserId: 'owner-user',
|
|
57
|
+
defaultTenantId: tenantId,
|
|
58
|
+
ownerTokenHash: 'hash',
|
|
59
|
+
idlePauseMs: 0,
|
|
60
|
+
wakeTimeoutMs: 30_000,
|
|
61
|
+
readyTimeoutMs: 2_000,
|
|
62
|
+
cookieSecure: true,
|
|
63
|
+
seedHome: null,
|
|
64
|
+
},
|
|
65
|
+
m0: {
|
|
66
|
+
providerName: 'none',
|
|
67
|
+
registryPath,
|
|
68
|
+
guestUser: 'agent',
|
|
69
|
+
guestHome: '/home/agent',
|
|
70
|
+
guestCodePath: '/opt/crouter',
|
|
71
|
+
webPort: 7878,
|
|
72
|
+
nodeKind: 'general',
|
|
73
|
+
piPackages: [],
|
|
74
|
+
piAuthJson: null,
|
|
75
|
+
relayToken: null,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function setup() {
|
|
80
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-hearth-warm-'));
|
|
81
|
+
const registryPath = join(tempDir, 'homes.json');
|
|
82
|
+
const statePath = join(tempDir, 'state.json');
|
|
83
|
+
writeFileSync(registryPath, `${JSON.stringify({ [tenantId]: makeHome() }, null, 2)}\n`, 'utf8');
|
|
84
|
+
// configureWakeProxyRuntime resets the warm-verdict/in-flight caches, isolating each test.
|
|
85
|
+
configureWakeProxyRuntime(makeRuntime(registryPath, statePath));
|
|
86
|
+
}
|
|
87
|
+
const awakened = { tenantId, nodeId, routeUrl, relayToken: 'relay-token', machineId, webPort: 7878 };
|
|
88
|
+
test('concurrent ensureAwake calls coalesce onto ONE slow probe instead of N serial probes', async (t) => {
|
|
89
|
+
setup();
|
|
90
|
+
let releaseProbe;
|
|
91
|
+
const probeGate = new Promise((resolve) => {
|
|
92
|
+
releaseProbe = resolve;
|
|
93
|
+
});
|
|
94
|
+
const probe = t.mock.method(wakeProxyInternals, 'probeRelayServing', async () => {
|
|
95
|
+
await probeGate;
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
t.mock.method(wakeProxyInternals, 'touchTenantWake', async () => undefined);
|
|
99
|
+
t.mock.method(wakeProxyInternals, 'captureAwakeHome', async () => awakened);
|
|
100
|
+
const wake = t.mock.method(wakeProxyInternals, 'wakeHome', async () => {
|
|
101
|
+
throw new Error('wakeHome should not run on the warm path');
|
|
102
|
+
});
|
|
103
|
+
// Fire five concurrent callers while the (slow) probe is still gated. They must all share the
|
|
104
|
+
// single in-flight wake — only one probe runs, not five serialized behind the lock.
|
|
105
|
+
const calls = [
|
|
106
|
+
ensureAwake(tenantId),
|
|
107
|
+
ensureAwake(tenantId),
|
|
108
|
+
ensureAwake(tenantId),
|
|
109
|
+
ensureAwake(tenantId),
|
|
110
|
+
ensureAwake(tenantId),
|
|
111
|
+
];
|
|
112
|
+
// The lock dispatches runEnsureAwake on a microtask, so let that settle before counting; the
|
|
113
|
+
// probe is gated so it stays at exactly one in-flight call.
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
115
|
+
const probeCallsWhileGated = probe.mock.callCount();
|
|
116
|
+
releaseProbe();
|
|
117
|
+
const results = await Promise.all(calls);
|
|
118
|
+
assert.equal(probeCallsWhileGated, 1);
|
|
119
|
+
assert.equal(probe.mock.callCount(), 1);
|
|
120
|
+
assert.equal(wake.mock.callCount(), 0);
|
|
121
|
+
for (const result of results)
|
|
122
|
+
assert.equal(result.routeUrl, routeUrl);
|
|
123
|
+
});
|
|
124
|
+
test('a fresh warm verdict serves concurrent reads WITHOUT re-probing or waiting on the slow path', async (t) => {
|
|
125
|
+
setup();
|
|
126
|
+
// Freeze time so the verdict stays inside its TTL across the test.
|
|
127
|
+
t.mock.method(wakeProxyInternals, 'now', () => 1_000);
|
|
128
|
+
const probe = t.mock.method(wakeProxyInternals, 'probeRelayServing', async () => true);
|
|
129
|
+
t.mock.method(wakeProxyInternals, 'touchTenantWake', async () => undefined);
|
|
130
|
+
t.mock.method(wakeProxyInternals, 'captureAwakeHome', async () => awakened);
|
|
131
|
+
// Prime: first call probes once and records the warm verdict.
|
|
132
|
+
await ensureAwake(tenantId);
|
|
133
|
+
assert.equal(probe.mock.callCount(), 1);
|
|
134
|
+
// Concurrent reads inside the TTL must resolve purely from the warm verdict — no extra probe,
|
|
135
|
+
// no re-entering the slow wake path. A regression that dropped back into the lock would bump the
|
|
136
|
+
// probe count past 1 here.
|
|
137
|
+
const results = await Promise.all([ensureAwake(tenantId), ensureAwake(tenantId), ensureAwake(tenantId)]);
|
|
138
|
+
assert.equal(probe.mock.callCount(), 1);
|
|
139
|
+
for (const result of results)
|
|
140
|
+
assert.equal(result.routeUrl, routeUrl);
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|