agent-tempo 1.0.1
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/CLAUDE.md +213 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/icon-dark-32.png +0 -0
- package/assets/icon-dark-64.png +0 -0
- package/assets/icon-dark.svg +9 -0
- package/assets/icon.svg +9 -0
- package/assets/logo-dark.svg +11 -0
- package/assets/logo-light.svg +11 -0
- package/dashboard/README.md +91 -0
- package/dashboard/dist/assets/index-CB78ToNE.css +2 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js +62 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js.map +1 -0
- package/dashboard/dist/index.html +21 -0
- package/dashboard/package.json +47 -0
- package/dist/activities/hard-terminate.d.ts +32 -0
- package/dist/activities/hard-terminate.js +460 -0
- package/dist/activities/maestro.d.ts +72 -0
- package/dist/activities/maestro.js +254 -0
- package/dist/activities/outbox.d.ts +188 -0
- package/dist/activities/outbox.js +849 -0
- package/dist/activities/resolve.d.ts +64 -0
- package/dist/activities/resolve.js +129 -0
- package/dist/activities/schedule-fire.d.ts +36 -0
- package/dist/activities/schedule-fire.js +147 -0
- package/dist/adapters/base.d.ts +426 -0
- package/dist/adapters/base.js +1270 -0
- package/dist/adapters/claude-api/adapter.d.ts +168 -0
- package/dist/adapters/claude-api/adapter.js +797 -0
- package/dist/adapters/claude-api/api-error.d.ts +96 -0
- package/dist/adapters/claude-api/api-error.js +191 -0
- package/dist/adapters/claude-api/index.d.ts +16 -0
- package/dist/adapters/claude-api/index.js +21 -0
- package/dist/adapters/claude-api/mcp-bridge.d.ts +50 -0
- package/dist/adapters/claude-api/mcp-bridge.js +157 -0
- package/dist/adapters/claude-code/adapter.d.ts +133 -0
- package/dist/adapters/claude-code/adapter.js +274 -0
- package/dist/adapters/claude-code/index.d.ts +15 -0
- package/dist/adapters/claude-code/index.js +20 -0
- package/dist/adapters/claude-code-headless/adapter.d.ts +131 -0
- package/dist/adapters/claude-code-headless/adapter.js +710 -0
- package/dist/adapters/claude-code-headless/error-mapper.d.ts +107 -0
- package/dist/adapters/claude-code-headless/error-mapper.js +281 -0
- package/dist/adapters/claude-code-headless/index.d.ts +17 -0
- package/dist/adapters/claude-code-headless/index.js +26 -0
- package/dist/adapters/claude-code-headless/pre-flight.d.ts +51 -0
- package/dist/adapters/claude-code-headless/pre-flight.js +207 -0
- package/dist/adapters/claude-code-headless/prompt.d.ts +93 -0
- package/dist/adapters/claude-code-headless/prompt.js +79 -0
- package/dist/adapters/claude-code-headless/stream-json.d.ts +242 -0
- package/dist/adapters/claude-code-headless/stream-json.js +208 -0
- package/dist/adapters/claude-code-headless/types.d.ts +28 -0
- package/dist/adapters/claude-code-headless/types.js +36 -0
- package/dist/adapters/copilot/adapter.d.ts +100 -0
- package/dist/adapters/copilot/adapter.js +730 -0
- package/dist/adapters/copilot/index.d.ts +15 -0
- package/dist/adapters/copilot/index.js +20 -0
- package/dist/adapters/index.d.ts +42 -0
- package/dist/adapters/index.js +115 -0
- package/dist/adapters/opencode/adapter.d.ts +82 -0
- package/dist/adapters/opencode/adapter.js +710 -0
- package/dist/adapters/opencode/config.d.ts +90 -0
- package/dist/adapters/opencode/config.js +137 -0
- package/dist/adapters/opencode/helpers.d.ts +40 -0
- package/dist/adapters/opencode/helpers.js +144 -0
- package/dist/adapters/opencode/index.d.ts +12 -0
- package/dist/adapters/opencode/index.js +17 -0
- package/dist/adapters/opencode/server-bridge.d.ts +124 -0
- package/dist/adapters/opencode/server-bridge.js +216 -0
- package/dist/adapters/sdk/base.d.ts +95 -0
- package/dist/adapters/sdk/base.js +134 -0
- package/dist/adapters/sdk/system-prompt.d.ts +64 -0
- package/dist/adapters/sdk/system-prompt.js +78 -0
- package/dist/adapters/terminal-error.d.ts +27 -0
- package/dist/adapters/terminal-error.js +39 -0
- package/dist/channel.d.ts +3 -0
- package/dist/channel.js +48 -0
- package/dist/cli/commands.d.ts +245 -0
- package/dist/cli/commands.js +2438 -0
- package/dist/cli/config-command.d.ts +8 -0
- package/dist/cli/config-command.js +254 -0
- package/dist/cli/daemon-command.d.ts +57 -0
- package/dist/cli/daemon-command.js +493 -0
- package/dist/cli/daemon.d.ts +217 -0
- package/dist/cli/daemon.js +632 -0
- package/dist/cli/dashboard-command.d.ts +20 -0
- package/dist/cli/dashboard-command.js +241 -0
- package/dist/cli/dev-banner.d.ts +107 -0
- package/dist/cli/dev-banner.js +190 -0
- package/dist/cli/dev-mode-bootstrap.d.ts +29 -0
- package/dist/cli/dev-mode-bootstrap.js +36 -0
- package/dist/cli/dev-verbs.d.ts +43 -0
- package/dist/cli/dev-verbs.js +254 -0
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +158 -0
- package/dist/cli/legacy-migration.d.ts +35 -0
- package/dist/cli/legacy-migration.js +335 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +63 -0
- package/dist/cli/output.d.ts +12 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/preflight.d.ts +9 -0
- package/dist/cli/preflight.js +96 -0
- package/dist/cli/removed-verbs.d.ts +9 -0
- package/dist/cli/removed-verbs.js +78 -0
- package/dist/cli/sa-preflight.d.ts +99 -0
- package/dist/cli/sa-preflight.js +183 -0
- package/dist/cli/scenarios-command.d.ts +6 -0
- package/dist/cli/scenarios-command.js +167 -0
- package/dist/cli/startup.d.ts +112 -0
- package/dist/cli/startup.js +641 -0
- package/dist/cli/upgrade-command.d.ts +5 -0
- package/dist/cli/upgrade-command.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +680 -0
- package/dist/client/core.d.ts +33 -0
- package/dist/client/core.js +1260 -0
- package/dist/client/ensure-conductor-spawned.d.ts +35 -0
- package/dist/client/ensure-conductor-spawned.js +48 -0
- package/dist/client/index.d.ts +32 -0
- package/dist/client/index.js +22 -0
- package/dist/client/interface.d.ts +461 -0
- package/dist/client/interface.js +2 -0
- package/dist/client/subscribe.d.ts +108 -0
- package/dist/client/subscribe.js +598 -0
- package/dist/client/with-spawn.d.ts +27 -0
- package/dist/client/with-spawn.js +87 -0
- package/dist/config.d.ts +323 -0
- package/dist/config.js +593 -0
- package/dist/connection.d.ts +7 -0
- package/dist/connection.js +46 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.js +74 -0
- package/dist/copilot-bridge.d.ts +22 -0
- package/dist/copilot-bridge.js +565 -0
- package/dist/daemon-adapter-versions.d.ts +52 -0
- package/dist/daemon-adapter-versions.js +170 -0
- package/dist/daemon.d.ts +275 -0
- package/dist/daemon.js +989 -0
- package/dist/ensemble/agent-types.d.ts +23 -0
- package/dist/ensemble/agent-types.js +132 -0
- package/dist/ensemble/loader.d.ts +14 -0
- package/dist/ensemble/loader.js +140 -0
- package/dist/ensemble/saver.d.ts +49 -0
- package/dist/ensemble/saver.js +201 -0
- package/dist/ensemble/schema.d.ts +71 -0
- package/dist/ensemble/schema.js +3 -0
- package/dist/git-info.d.ts +4 -0
- package/dist/git-info.js +29 -0
- package/dist/http/aggregate.d.ts +319 -0
- package/dist/http/aggregate.js +684 -0
- package/dist/http/auth.d.ts +67 -0
- package/dist/http/auth.js +177 -0
- package/dist/http/body.d.ts +71 -0
- package/dist/http/body.js +121 -0
- package/dist/http/catalog.d.ts +67 -0
- package/dist/http/catalog.js +209 -0
- package/dist/http/cors.d.ts +42 -0
- package/dist/http/cors.js +111 -0
- package/dist/http/dashboard-pair.d.ts +94 -0
- package/dist/http/dashboard-pair.js +148 -0
- package/dist/http/dashboard.d.ts +20 -0
- package/dist/http/dashboard.js +160 -0
- package/dist/http/event-bus.d.ts +217 -0
- package/dist/http/event-bus.js +365 -0
- package/dist/http/event-id.d.ts +77 -0
- package/dist/http/event-id.js +117 -0
- package/dist/http/event-types.d.ts +348 -0
- package/dist/http/event-types.js +36 -0
- package/dist/http/fixtures/chat-stress.d.ts +8 -0
- package/dist/http/fixtures/chat-stress.js +63 -0
- package/dist/http/fixtures/conductor-leaving.d.ts +8 -0
- package/dist/http/fixtures/conductor-leaving.js +80 -0
- package/dist/http/fixtures/constants.d.ts +10 -0
- package/dist/http/fixtures/constants.js +13 -0
- package/dist/http/fixtures/eight-player-broadcast.d.ts +10 -0
- package/dist/http/fixtures/eight-player-broadcast.js +81 -0
- package/dist/http/fixtures/empty-ensemble.d.ts +6 -0
- package/dist/http/fixtures/empty-ensemble.js +26 -0
- package/dist/http/fixtures/index.d.ts +55 -0
- package/dist/http/fixtures/index.js +110 -0
- package/dist/http/fixtures/single-conductor.d.ts +7 -0
- package/dist/http/fixtures/single-conductor.js +46 -0
- package/dist/http/fixtures/sse-reconnect.d.ts +8 -0
- package/dist/http/fixtures/sse-reconnect.js +77 -0
- package/dist/http/index.d.ts +21 -0
- package/dist/http/index.js +61 -0
- package/dist/http/port-file.d.ts +22 -0
- package/dist/http/port-file.js +132 -0
- package/dist/http/responses.d.ts +27 -0
- package/dist/http/responses.js +40 -0
- package/dist/http/ring-buffer.d.ts +41 -0
- package/dist/http/ring-buffer.js +80 -0
- package/dist/http/server.d.ts +122 -0
- package/dist/http/server.js +459 -0
- package/dist/http/snapshot.d.ts +85 -0
- package/dist/http/snapshot.js +180 -0
- package/dist/http/sse-handler.d.ts +87 -0
- package/dist/http/sse-handler.js +294 -0
- package/dist/http/writes.d.ts +55 -0
- package/dist/http/writes.js +240 -0
- package/dist/palette/index.d.ts +138 -0
- package/dist/palette/index.js +221 -0
- package/dist/reconcile/orphans.d.ts +255 -0
- package/dist/reconcile/orphans.js +340 -0
- package/dist/scripts/258-spotcheck.js +303 -0
- package/dist/scripts/check-components-css-sync.js +199 -0
- package/dist/scripts/run-shard.js +121 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +128 -0
- package/dist/server-tools.d.ts +87 -0
- package/dist/server-tools.js +146 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +366 -0
- package/dist/spawn.d.ts +296 -0
- package/dist/spawn.js +747 -0
- package/dist/tools/agent-types.d.ts +2 -0
- package/dist/tools/agent-types.js +21 -0
- package/dist/tools/attachment-info.d.ts +4 -0
- package/dist/tools/attachment-info.js +48 -0
- package/dist/tools/broadcast.d.ts +4 -0
- package/dist/tools/broadcast.js +76 -0
- package/dist/tools/cancel-stage.d.ts +3 -0
- package/dist/tools/cancel-stage.js +20 -0
- package/dist/tools/clear-state.d.ts +3 -0
- package/dist/tools/clear-state.js +37 -0
- package/dist/tools/coat-check-evict.d.ts +4 -0
- package/dist/tools/coat-check-evict.js +43 -0
- package/dist/tools/coat-check-get.d.ts +4 -0
- package/dist/tools/coat-check-get.js +56 -0
- package/dist/tools/coat-check-list.d.ts +4 -0
- package/dist/tools/coat-check-list.js +60 -0
- package/dist/tools/coat-check-put.d.ts +4 -0
- package/dist/tools/coat-check-put.js +53 -0
- package/dist/tools/cue.d.ts +44 -0
- package/dist/tools/cue.js +201 -0
- package/dist/tools/destroy.d.ts +4 -0
- package/dist/tools/destroy.js +188 -0
- package/dist/tools/detach.d.ts +4 -0
- package/dist/tools/detach.js +45 -0
- package/dist/tools/encore.d.ts +4 -0
- package/dist/tools/encore.js +31 -0
- package/dist/tools/ensemble.d.ts +32 -0
- package/dist/tools/ensemble.js +198 -0
- package/dist/tools/evaluate-gate.d.ts +3 -0
- package/dist/tools/evaluate-gate.js +32 -0
- package/dist/tools/fetch-state.d.ts +13 -0
- package/dist/tools/fetch-state.js +78 -0
- package/dist/tools/gates.d.ts +3 -0
- package/dist/tools/gates.js +41 -0
- package/dist/tools/helpers.d.ts +21 -0
- package/dist/tools/helpers.js +25 -0
- package/dist/tools/hosts.d.ts +4 -0
- package/dist/tools/hosts.js +40 -0
- package/dist/tools/listen.d.ts +3 -0
- package/dist/tools/listen.js +22 -0
- package/dist/tools/load-lineup.d.ts +5 -0
- package/dist/tools/load-lineup.js +381 -0
- package/dist/tools/migrate.d.ts +4 -0
- package/dist/tools/migrate.js +60 -0
- package/dist/tools/pause-ensemble.d.ts +4 -0
- package/dist/tools/pause-ensemble.js +58 -0
- package/dist/tools/pause.d.ts +4 -0
- package/dist/tools/pause.js +36 -0
- package/dist/tools/play.d.ts +4 -0
- package/dist/tools/play.js +57 -0
- package/dist/tools/quality-gate.d.ts +3 -0
- package/dist/tools/quality-gate.js +26 -0
- package/dist/tools/recall.d.ts +3 -0
- package/dist/tools/recall.js +32 -0
- package/dist/tools/recruit.d.ts +38 -0
- package/dist/tools/recruit.js +447 -0
- package/dist/tools/release.d.ts +4 -0
- package/dist/tools/release.js +98 -0
- package/dist/tools/report.d.ts +3 -0
- package/dist/tools/report.js +29 -0
- package/dist/tools/resolve.d.ts +1 -0
- package/dist/tools/resolve.js +7 -0
- package/dist/tools/restart.d.ts +35 -0
- package/dist/tools/restart.js +131 -0
- package/dist/tools/restore.d.ts +4 -0
- package/dist/tools/restore.js +107 -0
- package/dist/tools/resume-ensemble.d.ts +4 -0
- package/dist/tools/resume-ensemble.js +79 -0
- package/dist/tools/save-lineup.d.ts +4 -0
- package/dist/tools/save-lineup.js +36 -0
- package/dist/tools/save-state.d.ts +3 -0
- package/dist/tools/save-state.js +57 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.js +152 -0
- package/dist/tools/schedules.d.ts +4 -0
- package/dist/tools/schedules.js +54 -0
- package/dist/tools/set-ensemble-description.d.ts +4 -0
- package/dist/tools/set-ensemble-description.js +37 -0
- package/dist/tools/set-name.d.ts +4 -0
- package/dist/tools/set-name.js +45 -0
- package/dist/tools/set-part.d.ts +3 -0
- package/dist/tools/set-part.js +20 -0
- package/dist/tools/shutdown.d.ts +4 -0
- package/dist/tools/shutdown.js +54 -0
- package/dist/tools/stage.d.ts +3 -0
- package/dist/tools/stage.js +28 -0
- package/dist/tools/stages.d.ts +3 -0
- package/dist/tools/stages.js +35 -0
- package/dist/tools/stop.d.ts +4 -0
- package/dist/tools/stop.js +29 -0
- package/dist/tools/unschedule.d.ts +4 -0
- package/dist/tools/unschedule.js +35 -0
- package/dist/tools/who-am-i.d.ts +3 -0
- package/dist/tools/who-am-i.js +34 -0
- package/dist/tools/worktree.d.ts +4 -0
- package/dist/tools/worktree.js +181 -0
- package/dist/tui/App.d.ts +85 -0
- package/dist/tui/App.js +1791 -0
- package/dist/tui/bootstrap-types.d.ts +46 -0
- package/dist/tui/bootstrap-types.js +7 -0
- package/dist/tui/client.d.ts +6 -0
- package/dist/tui/client.js +9 -0
- package/dist/tui/commands.d.ts +71 -0
- package/dist/tui/commands.js +1375 -0
- package/dist/tui/components/ActivityLog.d.ts +16 -0
- package/dist/tui/components/ActivityLog.js +36 -0
- package/dist/tui/components/ChatView.d.ts +35 -0
- package/dist/tui/components/ChatView.js +54 -0
- package/dist/tui/components/CommandOverlay.d.ts +15 -0
- package/dist/tui/components/CommandOverlay.js +34 -0
- package/dist/tui/components/CommandPalette.d.ts +21 -0
- package/dist/tui/components/CommandPalette.js +67 -0
- package/dist/tui/components/ConductorChat.d.ts +16 -0
- package/dist/tui/components/ConductorChat.js +32 -0
- package/dist/tui/components/ConversationStream.d.ts +114 -0
- package/dist/tui/components/ConversationStream.js +307 -0
- package/dist/tui/components/CreateEnsembleWizard.d.ts +19 -0
- package/dist/tui/components/CreateEnsembleWizard.js +223 -0
- package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
- package/dist/tui/components/DestroyConfirmModal.js +62 -0
- package/dist/tui/components/EnsembleListView.d.ts +14 -0
- package/dist/tui/components/EnsembleListView.js +32 -0
- package/dist/tui/components/EnsemblePanel.d.ts +12 -0
- package/dist/tui/components/EnsemblePanel.js +40 -0
- package/dist/tui/components/ErrorView.d.ts +31 -0
- package/dist/tui/components/ErrorView.js +129 -0
- package/dist/tui/components/HomeView.d.ts +54 -0
- package/dist/tui/components/HomeView.js +306 -0
- package/dist/tui/components/InputBar.d.ts +13 -0
- package/dist/tui/components/InputBar.js +58 -0
- package/dist/tui/components/LoadLineupModal.d.ts +18 -0
- package/dist/tui/components/LoadLineupModal.js +79 -0
- package/dist/tui/components/MainView.d.ts +21 -0
- package/dist/tui/components/MainView.js +107 -0
- package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
- package/dist/tui/components/NewEnsembleModal.js +73 -0
- package/dist/tui/components/Picker.d.ts +23 -0
- package/dist/tui/components/Picker.js +70 -0
- package/dist/tui/components/PlayerDetailView.d.ts +26 -0
- package/dist/tui/components/PlayerDetailView.js +118 -0
- package/dist/tui/components/PromptArea.d.ts +50 -0
- package/dist/tui/components/PromptArea.js +303 -0
- package/dist/tui/components/RecruitWizard.d.ts +17 -0
- package/dist/tui/components/RecruitWizard.js +221 -0
- package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
- package/dist/tui/components/RestoreConfirmModal.js +71 -0
- package/dist/tui/components/ScheduleOverlay.d.ts +13 -0
- package/dist/tui/components/ScheduleOverlay.js +113 -0
- package/dist/tui/components/ScheduleWizard.d.ts +19 -0
- package/dist/tui/components/ScheduleWizard.js +259 -0
- package/dist/tui/components/Splash.d.ts +23 -0
- package/dist/tui/components/Splash.js +221 -0
- package/dist/tui/components/StatusBar.d.ts +48 -0
- package/dist/tui/components/StatusBar.js +128 -0
- package/dist/tui/components/StatusOverlay.d.ts +15 -0
- package/dist/tui/components/StatusOverlay.js +76 -0
- package/dist/tui/components/TitleBar.d.ts +10 -0
- package/dist/tui/components/TitleBar.js +21 -0
- package/dist/tui/components/TopBar.d.ts +12 -0
- package/dist/tui/components/TopBar.js +15 -0
- package/dist/tui/core-api.d.ts +26 -0
- package/dist/tui/core-api.js +67 -0
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +3 -0
- package/dist/tui/hooks/useEnsembleDiscovery.js +30 -0
- package/dist/tui/hooks/useMaestroPoller.d.ts +3 -0
- package/dist/tui/hooks/useMaestroPoller.js +36 -0
- package/dist/tui/hooks/useSendCommand.d.ts +7 -0
- package/dist/tui/hooks/useSendCommand.js +29 -0
- package/dist/tui/index.d.ts +15 -0
- package/dist/tui/index.js +156 -0
- package/dist/tui/ink-context.d.ts +18 -0
- package/dist/tui/ink-context.js +59 -0
- package/dist/tui/ink-loader.d.ts +26 -0
- package/dist/tui/ink-loader.js +42 -0
- package/dist/tui/removed-commands.d.ts +9 -0
- package/dist/tui/removed-commands.js +22 -0
- package/dist/tui/sse-handler.d.ts +52 -0
- package/dist/tui/sse-handler.js +157 -0
- package/dist/tui/store.d.ts +598 -0
- package/dist/tui/store.js +753 -0
- package/dist/tui/utils/format.d.ts +56 -0
- package/dist/tui/utils/format.js +155 -0
- package/dist/tui/utils/fullscreen.d.ts +23 -0
- package/dist/tui/utils/fullscreen.js +71 -0
- package/dist/tui/utils/history.d.ts +10 -0
- package/dist/tui/utils/history.js +85 -0
- package/dist/tui/utils/platform.d.ts +45 -0
- package/dist/tui/utils/platform.js +258 -0
- package/dist/tui/utils/theme.d.ts +21 -0
- package/dist/tui/utils/theme.js +24 -0
- package/dist/types.d.ts +1020 -0
- package/dist/types.js +39 -0
- package/dist/utils/attachment-format.d.ts +22 -0
- package/dist/utils/attachment-format.js +32 -0
- package/dist/utils/default-part.d.ts +43 -0
- package/dist/utils/default-part.js +104 -0
- package/dist/utils/duration.d.ts +30 -0
- package/dist/utils/duration.js +69 -0
- package/dist/utils/ensemble-ops.d.ts +61 -0
- package/dist/utils/ensemble-ops.js +77 -0
- package/dist/utils/format-hosts.d.ts +21 -0
- package/dist/utils/format-hosts.js +73 -0
- package/dist/utils/hosts.d.ts +113 -0
- package/dist/utils/hosts.js +265 -0
- package/dist/utils/parent-death-watchdog.d.ts +1 -0
- package/dist/utils/parent-death-watchdog.js +47 -0
- package/dist/utils/query-timeout.d.ts +103 -0
- package/dist/utils/query-timeout.js +113 -0
- package/dist/utils/recall-format.d.ts +78 -0
- package/dist/utils/recall-format.js +105 -0
- package/dist/utils/restore-format.d.ts +49 -0
- package/dist/utils/restore-format.js +91 -0
- package/dist/utils/safe-path.d.ts +10 -0
- package/dist/utils/safe-path.js +43 -0
- package/dist/utils/sdk-probe.d.ts +9 -0
- package/dist/utils/sdk-probe.js +45 -0
- package/dist/utils/search-attributes.d.ts +76 -0
- package/dist/utils/search-attributes.js +86 -0
- package/dist/utils/validation.d.ts +113 -0
- package/dist/utils/validation.js +163 -0
- package/dist/utils/visibility-deadline.d.ts +186 -0
- package/dist/utils/visibility-deadline.js +158 -0
- package/dist/utils/worktree.d.ts +103 -0
- package/dist/utils/worktree.js +327 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +146 -0
- package/dist/workflows/attachment-math.d.ts +56 -0
- package/dist/workflows/attachment-math.js +47 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +11 -0
- package/dist/workflows/maestro-signals.d.ts +217 -0
- package/dist/workflows/maestro-signals.js +155 -0
- package/dist/workflows/maestro.d.ts +3 -0
- package/dist/workflows/maestro.js +812 -0
- package/dist/workflows/scheduler-signals.d.ts +10 -0
- package/dist/workflows/scheduler-signals.js +14 -0
- package/dist/workflows/scheduler.d.ts +17 -0
- package/dist/workflows/scheduler.js +143 -0
- package/dist/workflows/session.d.ts +2 -0
- package/dist/workflows/session.js +1638 -0
- package/dist/workflows/signals.d.ts +297 -0
- package/dist/workflows/signals.js +239 -0
- package/examples/agents/tempo-composer.md +56 -0
- package/examples/agents/tempo-conductor.md +117 -0
- package/examples/agents/tempo-critic.md +73 -0
- package/examples/agents/tempo-improv.md +74 -0
- package/examples/agents/tempo-liner.md +75 -0
- package/examples/agents/tempo-roadie.md +61 -0
- package/examples/agents/tempo-soloist.md +71 -0
- package/examples/agents/tempo-tuner.md +94 -0
- package/examples/ensembles/tempo-big-band.yaml +146 -0
- package/examples/ensembles/tempo-dev-team.yaml +58 -0
- package/examples/ensembles/tempo-headless-jam.yaml +77 -0
- package/examples/ensembles/tempo-jam-session.yaml +41 -0
- package/examples/ensembles/tempo-mock-jam.yaml +79 -0
- package/examples/ensembles/tempo-review-squad.yaml +32 -0
- package/package.json +172 -0
- package/packaging/launchd/com.agent.tempo.plist +46 -0
- package/packaging/systemd/agent-tempo.service +32 -0
- package/packaging/windows/install-task.ps1 +71 -0
- package/scenarios/conductor-recruit-mock.yaml +33 -0
- package/scenarios/echo-roundtrip.yaml +15 -0
- package/scenarios/multi-player-handoff.yaml +38 -0
- package/scenarios/recruit-cascade.yaml +38 -0
- package/scenarios/two-player-conversation.yaml +33 -0
- package/workflow-bundle.js +14146 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleListAgentTypes = handleListAgentTypes;
|
|
4
|
+
exports.handleListLineups = handleListLineups;
|
|
5
|
+
exports.handleCreateEnsemble = handleCreateEnsemble;
|
|
6
|
+
const responses_1 = require("./responses");
|
|
7
|
+
const agent_types_1 = require("../ensemble/agent-types");
|
|
8
|
+
const loader_1 = require("../ensemble/loader");
|
|
9
|
+
const saver_1 = require("../ensemble/saver");
|
|
10
|
+
const validation_1 = require("../utils/validation");
|
|
11
|
+
const body_1 = require("./body");
|
|
12
|
+
/**
|
|
13
|
+
* `listAgentTypes()` returns rows with `path` + `nativeResolvable` +
|
|
14
|
+
* optional `allowedTools` — the dashboard only needs name/description/
|
|
15
|
+
* source. Strip the rest so we don't leak filesystem paths over the
|
|
16
|
+
* wire (loopback bind is the common case but the API is also reachable
|
|
17
|
+
* over LAN behind bearer auth — same privacy contract as `HostProfile`
|
|
18
|
+
* stripping in `src/types.ts`).
|
|
19
|
+
*/
|
|
20
|
+
function handleListAgentTypes(res) {
|
|
21
|
+
const types = (0, agent_types_1.listAgentTypes)();
|
|
22
|
+
const rows = types.map((t) => ({
|
|
23
|
+
name: t.name,
|
|
24
|
+
...(t.description !== undefined && { description: t.description }),
|
|
25
|
+
source: t.source,
|
|
26
|
+
}));
|
|
27
|
+
(0, responses_1.jsonResponse)(res, 200, { agentTypes: rows });
|
|
28
|
+
}
|
|
29
|
+
function handleListLineups(res) {
|
|
30
|
+
// `listAllLineups()` returns a richer shape (`path` + parsed
|
|
31
|
+
// `description` / `players`); strip `path` before serving over the
|
|
32
|
+
// wire — privacy contract parity with the agent-types handler above.
|
|
33
|
+
const rows = (0, saver_1.listAllLineups)().map((l) => ({
|
|
34
|
+
name: l.name,
|
|
35
|
+
...(l.description !== undefined && { description: l.description }),
|
|
36
|
+
players: l.players,
|
|
37
|
+
source: l.source,
|
|
38
|
+
}));
|
|
39
|
+
(0, responses_1.jsonResponse)(res, 200, { lineups: rows });
|
|
40
|
+
}
|
|
41
|
+
// ── POST /v1/ensembles ──────────────────────────────────────────────────────
|
|
42
|
+
const ALLOWED_START_MODES = ['hold', 'release'];
|
|
43
|
+
/**
|
|
44
|
+
* POST `/v1/ensembles` — create a fresh ensemble.
|
|
45
|
+
*
|
|
46
|
+
* The bootstrap path is the daemon-side equivalent of the CLI's
|
|
47
|
+
* `agent-tempo up` codepath, but trimmed to the actions the daemon
|
|
48
|
+
* can run for a browser caller:
|
|
49
|
+
*
|
|
50
|
+
* 1. Validate the body (name regex, lineup exists if specified,
|
|
51
|
+
* startMode in {hold, release}).
|
|
52
|
+
* 2. Reject 409 if an ensemble with that name is already live.
|
|
53
|
+
* 3. Recruit the conductor (`isConductor: true`) — this bootstraps
|
|
54
|
+
* the maestro + scheduler + conductor-session workflows.
|
|
55
|
+
* 4. Recruit each lineup player. Inherits `host` + `held` from the
|
|
56
|
+
* top-level body when the lineup row doesn't specify its own.
|
|
57
|
+
* 5. Return 201 + the ensemble summary so the dashboard can
|
|
58
|
+
* navigate to `/ensemble/:name` without a follow-up snapshot
|
|
59
|
+
* fetch.
|
|
60
|
+
*
|
|
61
|
+
* Skipped vs CLI `up` (intentional):
|
|
62
|
+
* - No Temporal-server start: the daemon serving this request
|
|
63
|
+
* already proves Temporal is up.
|
|
64
|
+
* - No daemon start / agent-type install / MCP register: a browser
|
|
65
|
+
* caller doesn't go through that pre-flight.
|
|
66
|
+
* - No interactive "ensemble already exists" choice tree: HTTP is
|
|
67
|
+
* stateless; we 409 and let the dashboard surface a useful error.
|
|
68
|
+
*/
|
|
69
|
+
async function handleCreateEnsemble(req, res, client) {
|
|
70
|
+
const body = await (0, body_1.readJsonBody)(req);
|
|
71
|
+
if (body === body_1.BODY_TOO_LARGE) {
|
|
72
|
+
return (0, responses_1.errorResponse)(res, 413, { error: 'body-too-large', limit: body_1.WRITE_BODY_MAX });
|
|
73
|
+
}
|
|
74
|
+
if (body === body_1.BODY_INVALID_JSON) {
|
|
75
|
+
return (0, responses_1.errorResponse)(res, 400, { error: 'invalid-json' });
|
|
76
|
+
}
|
|
77
|
+
const parsed = parseBody(body);
|
|
78
|
+
if ('error' in parsed)
|
|
79
|
+
return (0, responses_1.errorResponse)(res, 400, parsed.error);
|
|
80
|
+
const { name, lineup, host, startMode, conductorInstructions } = parsed.body;
|
|
81
|
+
// 409 — ensemble already exists. `listEnsembles()` returns live
|
|
82
|
+
// ensembles by Temporal workflow; matches what the dashboard's
|
|
83
|
+
// `/v1/ensembles` GET sees. Note: TOCTOU between this read and the
|
|
84
|
+
// recruit below — two concurrent POSTs with the same name could both
|
|
85
|
+
// pass and both recruit. Acceptable for the browser-driven flow
|
|
86
|
+
// (user-paced); the proper guard is recruit-side idempotency, not
|
|
87
|
+
// here.
|
|
88
|
+
const existing = await client.listEnsembles();
|
|
89
|
+
if (existing.some((e) => e.name === name)) {
|
|
90
|
+
return (0, responses_1.errorResponse)(res, 409, { error: 'ensemble-exists', ensemble: name });
|
|
91
|
+
}
|
|
92
|
+
// Resolve lineup if specified. We `loadLineup` here (not at recruit
|
|
93
|
+
// time) so the caller gets a clean 400 for malformed lineups instead
|
|
94
|
+
// of a half-bootstrapped ensemble with the conductor already alive.
|
|
95
|
+
let resolved = null;
|
|
96
|
+
if (lineup) {
|
|
97
|
+
try {
|
|
98
|
+
const path = (0, loader_1.resolveLineupPath)(lineup).path;
|
|
99
|
+
resolved = (0, loader_1.loadLineup)(path);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return (0, responses_1.errorResponse)(res, 400, {
|
|
103
|
+
error: 'invalid-lineup',
|
|
104
|
+
lineup,
|
|
105
|
+
message: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const held = startMode === 'hold';
|
|
110
|
+
const allowed = (0, body_1.allowedAgentsForCurrentMode)();
|
|
111
|
+
// 1. Recruit the conductor — bootstraps maestro + scheduler. Lineup's
|
|
112
|
+
// `conductor` block (if present) wins on name + agent + part; the
|
|
113
|
+
// top-level `host` is the conductor's spawn target either way.
|
|
114
|
+
const conductorBlock = resolved?.conductor;
|
|
115
|
+
const conductorName = conductorBlock?.name ?? 'conductor';
|
|
116
|
+
const conductorAgent = pickAgent(conductorBlock?.agent, allowed);
|
|
117
|
+
try {
|
|
118
|
+
await client.recruit(name, {
|
|
119
|
+
name: conductorName,
|
|
120
|
+
workDir: process.cwd(),
|
|
121
|
+
isConductor: true,
|
|
122
|
+
...(conductorAgent !== undefined && { agent: conductorAgent }),
|
|
123
|
+
...(host !== undefined && { host }),
|
|
124
|
+
...(held && { held: true }),
|
|
125
|
+
...(conductorInstructions !== undefined && { initialMessage: conductorInstructions }),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return (0, responses_1.errorResponse)(res, 500, {
|
|
130
|
+
error: 'conductor-recruit-failed',
|
|
131
|
+
message: err instanceof Error ? err.message : String(err),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 2. Recruit each lineup player. Errors here are non-fatal — the
|
|
135
|
+
// ensemble is already alive (conductor is running); we collect
|
|
136
|
+
// failures and return them in the 201 body so the dashboard can
|
|
137
|
+
// surface a partial-success toast without rolling back the
|
|
138
|
+
// conductor. Parallel fan-out keeps a 5+ player ensemble from
|
|
139
|
+
// stacking N sequential Temporal round-trips on the client.
|
|
140
|
+
const lineupPlayers = resolved?.players ?? [];
|
|
141
|
+
const playerErrors = [];
|
|
142
|
+
const settled = await Promise.allSettled(lineupPlayers.map((player) => {
|
|
143
|
+
const playerAgent = pickAgent(player.agent, allowed);
|
|
144
|
+
return client.recruit(name, {
|
|
145
|
+
name: player.name,
|
|
146
|
+
workDir: player.workDir ?? process.cwd(),
|
|
147
|
+
...(playerAgent !== undefined && { agent: playerAgent }),
|
|
148
|
+
...(player.type !== undefined && { playerType: player.type }),
|
|
149
|
+
...(host !== undefined && { host }),
|
|
150
|
+
...(held && { held: true }),
|
|
151
|
+
...(player.instructions !== undefined && { systemPrompt: player.instructions }),
|
|
152
|
+
});
|
|
153
|
+
}));
|
|
154
|
+
settled.forEach((result, i) => {
|
|
155
|
+
if (result.status === 'rejected') {
|
|
156
|
+
playerErrors.push({
|
|
157
|
+
player: lineupPlayers[i].name,
|
|
158
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
(0, responses_1.jsonResponse)(res, 201, {
|
|
163
|
+
ensemble: name,
|
|
164
|
+
conductorPlayerId: conductorName,
|
|
165
|
+
lineup: lineup ?? null,
|
|
166
|
+
recruitedPlayers: lineupPlayers.length - playerErrors.length,
|
|
167
|
+
...(playerErrors.length > 0 && { playerErrors }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function parseBody(body) {
|
|
171
|
+
// `requireNonEmpty` filters whitespace-zero strings — empty lineup /
|
|
172
|
+
// host / instructions shouldn't propagate as if the user typed
|
|
173
|
+
// something.
|
|
174
|
+
const name = (0, body_1.stringField)(body, 'name', { requireNonEmpty: true });
|
|
175
|
+
if (!name)
|
|
176
|
+
return { error: { error: 'missing-field', field: 'name' } };
|
|
177
|
+
if ((0, validation_1.validateEnsembleName)(name) !== null) {
|
|
178
|
+
return { error: { error: 'invalid-ensemble-name', field: 'name' } };
|
|
179
|
+
}
|
|
180
|
+
const lineup = (0, body_1.stringField)(body, 'lineup', { requireNonEmpty: true });
|
|
181
|
+
const host = (0, body_1.stringField)(body, 'host', { requireNonEmpty: true });
|
|
182
|
+
const startMode = (0, body_1.stringField)(body, 'startMode', { requireNonEmpty: true });
|
|
183
|
+
if (startMode !== undefined && !isStartMode(startMode)) {
|
|
184
|
+
return { error: { error: 'invalid-start-mode', allowed: ALLOWED_START_MODES } };
|
|
185
|
+
}
|
|
186
|
+
const conductorInstructions = (0, body_1.stringField)(body, 'conductorInstructions', { requireNonEmpty: true });
|
|
187
|
+
// Lightweight host-name shape check (matches the daemon's existing
|
|
188
|
+
// hostname rules used by recruit). Skips when omitted.
|
|
189
|
+
if (host !== undefined && (0, validation_1.validatePlayerName)(host) !== null) {
|
|
190
|
+
return { error: { error: 'invalid-host', field: 'host' } };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
body: {
|
|
194
|
+
name,
|
|
195
|
+
...(lineup !== undefined && { lineup }),
|
|
196
|
+
...(host !== undefined && { host }),
|
|
197
|
+
...(startMode !== undefined && { startMode: startMode }),
|
|
198
|
+
...(conductorInstructions !== undefined && { conductorInstructions }),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function isStartMode(s) {
|
|
203
|
+
return ALLOWED_START_MODES.includes(s);
|
|
204
|
+
}
|
|
205
|
+
function pickAgent(candidate, allowed) {
|
|
206
|
+
if (candidate === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
return (0, body_1.isAllowedAgent)(candidate, allowed) ? candidate : undefined;
|
|
209
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface CorsConfig {
|
|
2
|
+
/** Explicit list from `AGENT_TEMPO_CORS_ORIGINS`. Empty → defaults only. */
|
|
3
|
+
allowedOrigins: string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse the comma-separated env var. Empty / undefined → empty list.
|
|
7
|
+
* Trims whitespace; rejects empty entries; preserves case (Origin
|
|
8
|
+
* comparison is case-sensitive per RFC 6454 §4).
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCorsOrigins(raw: string | undefined): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Decide if `originHeader` is allowed. Pass `true` for `bearerActive` only
|
|
13
|
+
* when bearer mode applies to the request — loopback mode short-circuits
|
|
14
|
+
* to `true` (no enforcement).
|
|
15
|
+
*
|
|
16
|
+
* Returns the value to echo on `Access-Control-Allow-Origin`, or `null`
|
|
17
|
+
* if the origin should be rejected. The default loopback allowlist
|
|
18
|
+
* (`localhost:*`, `127.0.0.1:*`) matches by hostname only; ports are
|
|
19
|
+
* always permitted because operators frequently bind dev servers to
|
|
20
|
+
* arbitrary ports.
|
|
21
|
+
*/
|
|
22
|
+
export declare function evaluateOrigin(originHeader: string | undefined, config: CorsConfig, bearerActive: boolean): {
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
echo: string | null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Static CORS response headers — independent of the request's Origin.
|
|
28
|
+
* The dynamic `Access-Control-Allow-Origin` comes from
|
|
29
|
+
* {@link evaluateOrigin}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function corsResponseHeaders(): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Convenience wrapper — should the daemon enforce CORS at all? `true`
|
|
34
|
+
* iff the bind addr is non-loopback (every connection is bearer-mode)
|
|
35
|
+
* OR the operator opted into bearer mode by setting an explicit
|
|
36
|
+
* allowlist via `AGENT_TEMPO_CORS_ORIGINS`.
|
|
37
|
+
*
|
|
38
|
+
* Used at boot time to decide whether to install the CORS middleware
|
|
39
|
+
* or no-op it. Per-request bearer determination still happens via
|
|
40
|
+
* {@link bearerRequired} — this helper is a startup short-circuit.
|
|
41
|
+
*/
|
|
42
|
+
export declare function corsEnforced(bindAddr: string, allowedOrigins: string[]): boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCorsOrigins = parseCorsOrigins;
|
|
4
|
+
exports.evaluateOrigin = evaluateOrigin;
|
|
5
|
+
exports.corsResponseHeaders = corsResponseHeaders;
|
|
6
|
+
exports.corsEnforced = corsEnforced;
|
|
7
|
+
/**
|
|
8
|
+
* CORS handling for the daemon HTTP surface (SSE-PROTOCOL.md §3.2).
|
|
9
|
+
*
|
|
10
|
+
* **Only enforced in bearer mode.** Loopback-mode requests skip CORS
|
|
11
|
+
* entirely — there's no cross-origin worry when the daemon and consumer
|
|
12
|
+
* share a host with no Origin or a loopback Origin.
|
|
13
|
+
*
|
|
14
|
+
* **Defaults**:
|
|
15
|
+
* - Allowlist: `localhost:*` and `127.0.0.1:*` echoed back
|
|
16
|
+
* - `Access-Control-Allow-Credentials: false` (non-configurable)
|
|
17
|
+
* - `Access-Control-Allow-Methods: GET, OPTIONS` (non-configurable)
|
|
18
|
+
* - `Access-Control-Allow-Headers: Authorization, Last-Event-ID` (non-configurable)
|
|
19
|
+
* - `Access-Control-Max-Age: 600` (non-configurable)
|
|
20
|
+
*
|
|
21
|
+
* Override the allowlist via `AGENT_TEMPO_CORS_ORIGINS` — comma-separated
|
|
22
|
+
* explicit origins (e.g. `https://dashboard.example.com,https://localhost:3000`).
|
|
23
|
+
* Wildcards are NOT supported. `*` is incompatible with credentials, and
|
|
24
|
+
* we want operators to opt every origin in deliberately.
|
|
25
|
+
*/
|
|
26
|
+
const auth_1 = require("./auth");
|
|
27
|
+
/** Hostnames that always pass the default allowlist. */
|
|
28
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
29
|
+
/**
|
|
30
|
+
* Parse the comma-separated env var. Empty / undefined → empty list.
|
|
31
|
+
* Trims whitespace; rejects empty entries; preserves case (Origin
|
|
32
|
+
* comparison is case-sensitive per RFC 6454 §4).
|
|
33
|
+
*/
|
|
34
|
+
function parseCorsOrigins(raw) {
|
|
35
|
+
if (!raw)
|
|
36
|
+
return [];
|
|
37
|
+
return raw
|
|
38
|
+
.split(',')
|
|
39
|
+
.map((s) => s.trim())
|
|
40
|
+
.filter((s) => s.length > 0);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide if `originHeader` is allowed. Pass `true` for `bearerActive` only
|
|
44
|
+
* when bearer mode applies to the request — loopback mode short-circuits
|
|
45
|
+
* to `true` (no enforcement).
|
|
46
|
+
*
|
|
47
|
+
* Returns the value to echo on `Access-Control-Allow-Origin`, or `null`
|
|
48
|
+
* if the origin should be rejected. The default loopback allowlist
|
|
49
|
+
* (`localhost:*`, `127.0.0.1:*`) matches by hostname only; ports are
|
|
50
|
+
* always permitted because operators frequently bind dev servers to
|
|
51
|
+
* arbitrary ports.
|
|
52
|
+
*/
|
|
53
|
+
function evaluateOrigin(originHeader, config, bearerActive) {
|
|
54
|
+
// Loopback mode: never enforce. No ACAO header set unless caller picks
|
|
55
|
+
// a permissive value — leave the decision to the response builder.
|
|
56
|
+
if (!bearerActive)
|
|
57
|
+
return { allowed: true, echo: null };
|
|
58
|
+
if (!originHeader) {
|
|
59
|
+
// Bearer mode but no Origin (e.g. server-to-server fetch). Allow
|
|
60
|
+
// by default; the bearer token still gates content. Browser fetches
|
|
61
|
+
// always include Origin — this branch is for non-browser callers.
|
|
62
|
+
return { allowed: true, echo: null };
|
|
63
|
+
}
|
|
64
|
+
// Explicit allowlist takes precedence.
|
|
65
|
+
if (config.allowedOrigins.length > 0) {
|
|
66
|
+
if (config.allowedOrigins.includes(originHeader)) {
|
|
67
|
+
return { allowed: true, echo: originHeader };
|
|
68
|
+
}
|
|
69
|
+
return { allowed: false, echo: null };
|
|
70
|
+
}
|
|
71
|
+
// Default: any port on a loopback hostname.
|
|
72
|
+
let host = null;
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(originHeader);
|
|
75
|
+
host = url.hostname;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { allowed: false, echo: null };
|
|
79
|
+
}
|
|
80
|
+
if (host && LOOPBACK_HOSTS.has(host)) {
|
|
81
|
+
return { allowed: true, echo: originHeader };
|
|
82
|
+
}
|
|
83
|
+
return { allowed: false, echo: null };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Static CORS response headers — independent of the request's Origin.
|
|
87
|
+
* The dynamic `Access-Control-Allow-Origin` comes from
|
|
88
|
+
* {@link evaluateOrigin}.
|
|
89
|
+
*/
|
|
90
|
+
function corsResponseHeaders() {
|
|
91
|
+
return {
|
|
92
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
93
|
+
'Access-Control-Allow-Headers': 'Authorization, Last-Event-ID',
|
|
94
|
+
'Access-Control-Allow-Credentials': 'false',
|
|
95
|
+
'Access-Control-Max-Age': '600',
|
|
96
|
+
Vary: 'Origin',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convenience wrapper — should the daemon enforce CORS at all? `true`
|
|
101
|
+
* iff the bind addr is non-loopback (every connection is bearer-mode)
|
|
102
|
+
* OR the operator opted into bearer mode by setting an explicit
|
|
103
|
+
* allowlist via `AGENT_TEMPO_CORS_ORIGINS`.
|
|
104
|
+
*
|
|
105
|
+
* Used at boot time to decide whether to install the CORS middleware
|
|
106
|
+
* or no-op it. Per-request bearer determination still happens via
|
|
107
|
+
* {@link bearerRequired} — this helper is a startup short-circuit.
|
|
108
|
+
*/
|
|
109
|
+
function corsEnforced(bindAddr, allowedOrigins) {
|
|
110
|
+
return !(0, auth_1.isLoopbackBindAddr)(bindAddr) || allowedOrigins.length > 0;
|
|
111
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard QR-code pairing flow — PR-8 of #340.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the 501 stub `handlePairTokenStub` from
|
|
5
|
+
* `src/http/dashboard.ts` (PR-1) with the real two-endpoint flow:
|
|
6
|
+
*
|
|
7
|
+
* POST /dashboard/api/pair (post-auth)
|
|
8
|
+
* The CLI invokes this when the operator runs `agent-tempo
|
|
9
|
+
* dashboard --pair`. Mints a single-use, 5-minute, base64url
|
|
10
|
+
* 32-byte token. The token is paired with the operator's bearer
|
|
11
|
+
* for delivery on consume.
|
|
12
|
+
*
|
|
13
|
+
* GET /dashboard/api/pair/:token (pre-auth carve-out, parallel
|
|
14
|
+
* to /v1/health)
|
|
15
|
+
* The dashboard SPA calls this on first mount when it sees
|
|
16
|
+
* `?pair=<token>` in the URL. Exchanges the token for the
|
|
17
|
+
* daemon's bearer + a TTL hint. Marks the token used so a
|
|
18
|
+
* second consume returns 410 Gone.
|
|
19
|
+
*
|
|
20
|
+
* State is in-memory (`Map<token, PendingPairing>`) — daemon restart
|
|
21
|
+
* invalidates outstanding pairings, which ADR 0013 documents as an
|
|
22
|
+
* acceptable trade for v1 (operator re-runs `--pair` after a
|
|
23
|
+
* restart; pairings are inherently transient).
|
|
24
|
+
*
|
|
25
|
+
* Token lifecycle:
|
|
26
|
+
* created → mint adds to `pendingPairings`
|
|
27
|
+
* consumed → first GET marks `used: true` + immediately deletes
|
|
28
|
+
* from the map so a leaked token can't even attempt a
|
|
29
|
+
* second exchange. (The check is "absent or used".)
|
|
30
|
+
* expired → swept every {@link PAIR_SWEEP_INTERVAL_MS} or
|
|
31
|
+
* opportunistically on consume; returns 410 in both
|
|
32
|
+
* cases.
|
|
33
|
+
*
|
|
34
|
+
* Security posture:
|
|
35
|
+
* - The token is 32 bytes of `crypto.randomBytes` rendered as
|
|
36
|
+
* base64url. ~256 bits of entropy; brute-force is infeasible
|
|
37
|
+
* within the 5-min window.
|
|
38
|
+
* - The token is short-TTL by design — we don't persist it across
|
|
39
|
+
* daemon restarts and we don't surface it on logs above prefix.
|
|
40
|
+
* - We never log the full token. `pair.minted` / `pair.consumed`
|
|
41
|
+
* log lines carry only the first 8 chars (`tokenPrefix`) for
|
|
42
|
+
* correlating with telemetry.
|
|
43
|
+
* - The mint endpoint requires a bearer (it's behind the standard
|
|
44
|
+
* auth gate). The consume endpoint does NOT require a bearer —
|
|
45
|
+
* the token IS the auth, parallel to `/v1/health` per
|
|
46
|
+
* `docs/design/340-web-dashboard.md` §4.1.
|
|
47
|
+
*/
|
|
48
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
49
|
+
/** TTL for a pending pairing — 5 minutes, hard ceiling. */
|
|
50
|
+
export declare const PAIR_TTL_MS: number;
|
|
51
|
+
/** How often the sweep removes expired entries; cheaper than per-request. */
|
|
52
|
+
export declare const PAIR_SWEEP_INTERVAL_MS: number;
|
|
53
|
+
/**
|
|
54
|
+
* Hard cap on simultaneous pending pairings. Defence-in-depth against
|
|
55
|
+
* an abusive operator (or a script in the operator's terminal)
|
|
56
|
+
* hammering `agent-tempo dashboard --pair` faster than the 60-second
|
|
57
|
+
* sweep can reclaim memory. With 5-min TTL the natural ceiling is one
|
|
58
|
+
* mint per second × 300 seconds = 300 entries; cap at 100 forces a
|
|
59
|
+
* 503 long before that becomes load-bearing.
|
|
60
|
+
*/
|
|
61
|
+
export declare const MAX_PENDING_PAIRINGS = 100;
|
|
62
|
+
/** Generate a fresh token. Exported for tests. */
|
|
63
|
+
export declare function mintPairToken(): string;
|
|
64
|
+
/**
|
|
65
|
+
* POST `/dashboard/api/pair` — mint a pairing. Caller is the operator
|
|
66
|
+
* via `agent-tempo dashboard --pair`; the request has already passed
|
|
67
|
+
* the bearer-auth gate so we know the operator is authorised. We
|
|
68
|
+
* accept the operator's bearer from the `Authorization` header and
|
|
69
|
+
* stamp it onto the pending entry so consume can hand it back.
|
|
70
|
+
*
|
|
71
|
+
* Response: `{ token, expiresAt }`. The CLI renders the QR.
|
|
72
|
+
*/
|
|
73
|
+
export declare function handlePairCreate(req: IncomingMessage, res: ServerResponse, bearer: string | null): void;
|
|
74
|
+
/**
|
|
75
|
+
* GET `/dashboard/api/pair/:token` — exchange the token for the bearer.
|
|
76
|
+
*
|
|
77
|
+
* Single-use: even if expired or never minted, return 410 Gone so an
|
|
78
|
+
* attacker can't distinguish "token never existed" from "token
|
|
79
|
+
* already consumed". The `error` field stays generic
|
|
80
|
+
* (`pair-token-invalid`) for the same reason.
|
|
81
|
+
*/
|
|
82
|
+
export declare function handlePairConsume(_req: IncomingMessage, res: ServerResponse, token: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Test escape hatch — wipe the in-memory map so each test starts
|
|
85
|
+
* from zero pendingPairings. Production code MUST NOT call this.
|
|
86
|
+
*
|
|
87
|
+
* Following the project's `__<verb><Noun>ForTests` naming convention
|
|
88
|
+
* (ADR 0006). Also stops the sweep timer so vitest exits cleanly.
|
|
89
|
+
*/
|
|
90
|
+
export declare function __resetPairingsForTests(): void;
|
|
91
|
+
/** Test-only — peek at the current count of pending pairings. */
|
|
92
|
+
export declare function __pendingPairingsCountForTests(): number;
|
|
93
|
+
/** Test-only — manually trigger the sweep at a synthetic `now`. */
|
|
94
|
+
export declare function __sweepExpiredForTests(now: number): number;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PENDING_PAIRINGS = exports.PAIR_SWEEP_INTERVAL_MS = exports.PAIR_TTL_MS = void 0;
|
|
4
|
+
exports.mintPairToken = mintPairToken;
|
|
5
|
+
exports.handlePairCreate = handlePairCreate;
|
|
6
|
+
exports.handlePairConsume = handlePairConsume;
|
|
7
|
+
exports.__resetPairingsForTests = __resetPairingsForTests;
|
|
8
|
+
exports.__pendingPairingsCountForTests = __pendingPairingsCountForTests;
|
|
9
|
+
exports.__sweepExpiredForTests = __sweepExpiredForTests;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const responses_1 = require("./responses");
|
|
12
|
+
/** TTL for a pending pairing — 5 minutes, hard ceiling. */
|
|
13
|
+
exports.PAIR_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
/** How often the sweep removes expired entries; cheaper than per-request. */
|
|
15
|
+
exports.PAIR_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
16
|
+
/** Bytes of entropy in the token; rendered as base64url (no padding). */
|
|
17
|
+
const PAIR_TOKEN_BYTES = 32;
|
|
18
|
+
/**
|
|
19
|
+
* Hard cap on simultaneous pending pairings. Defence-in-depth against
|
|
20
|
+
* an abusive operator (or a script in the operator's terminal)
|
|
21
|
+
* hammering `agent-tempo dashboard --pair` faster than the 60-second
|
|
22
|
+
* sweep can reclaim memory. With 5-min TTL the natural ceiling is one
|
|
23
|
+
* mint per second × 300 seconds = 300 entries; cap at 100 forces a
|
|
24
|
+
* 503 long before that becomes load-bearing.
|
|
25
|
+
*/
|
|
26
|
+
exports.MAX_PENDING_PAIRINGS = 100;
|
|
27
|
+
/**
|
|
28
|
+
* In-memory pending-pairing map. Module-level so a single daemon
|
|
29
|
+
* shares one map across requests; tests reset via
|
|
30
|
+
* `__resetPairingsForTests`.
|
|
31
|
+
*/
|
|
32
|
+
const pendingPairings = new Map();
|
|
33
|
+
let sweepTimer = null;
|
|
34
|
+
function scheduleSweep() {
|
|
35
|
+
if (sweepTimer)
|
|
36
|
+
return;
|
|
37
|
+
sweepTimer = setInterval(() => sweepExpired(), exports.PAIR_SWEEP_INTERVAL_MS);
|
|
38
|
+
// Don't keep the daemon process alive on a sweep timer alone.
|
|
39
|
+
if (typeof sweepTimer.unref === 'function')
|
|
40
|
+
sweepTimer.unref();
|
|
41
|
+
}
|
|
42
|
+
/** Strip every entry whose `expiresAt` is in the past. */
|
|
43
|
+
function sweepExpired(now = Date.now()) {
|
|
44
|
+
let removed = 0;
|
|
45
|
+
for (const [token, p] of pendingPairings) {
|
|
46
|
+
if (p.expiresAt <= now) {
|
|
47
|
+
pendingPairings.delete(token);
|
|
48
|
+
removed++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return removed;
|
|
52
|
+
}
|
|
53
|
+
/** Generate a fresh token. Exported for tests. */
|
|
54
|
+
function mintPairToken() {
|
|
55
|
+
return (0, crypto_1.randomBytes)(PAIR_TOKEN_BYTES).toString('base64url');
|
|
56
|
+
}
|
|
57
|
+
/** First 8 chars of the token — safe to log for telemetry correlation. */
|
|
58
|
+
function prefix(token) {
|
|
59
|
+
return token.slice(0, 8);
|
|
60
|
+
}
|
|
61
|
+
const log = (...args) => console.error('[agent-tempo:pair]', ...args);
|
|
62
|
+
/**
|
|
63
|
+
* POST `/dashboard/api/pair` — mint a pairing. Caller is the operator
|
|
64
|
+
* via `agent-tempo dashboard --pair`; the request has already passed
|
|
65
|
+
* the bearer-auth gate so we know the operator is authorised. We
|
|
66
|
+
* accept the operator's bearer from the `Authorization` header and
|
|
67
|
+
* stamp it onto the pending entry so consume can hand it back.
|
|
68
|
+
*
|
|
69
|
+
* Response: `{ token, expiresAt }`. The CLI renders the QR.
|
|
70
|
+
*/
|
|
71
|
+
function handlePairCreate(req, res, bearer) {
|
|
72
|
+
if (!bearer) {
|
|
73
|
+
// Defensive — the caller in `server.ts` already requires bearer
|
|
74
|
+
// mode for non-loopback binds. On loopback there's no bearer to
|
|
75
|
+
// hand back, so the pair flow is a no-op (the SPA on the host
|
|
76
|
+
// doesn't need a token; QR-code-from-mobile is the use case).
|
|
77
|
+
return (0, responses_1.errorResponse)(res, 400, {
|
|
78
|
+
error: 'pair-requires-bearer',
|
|
79
|
+
hint: 'pair flow only meaningful on bearer-protected binds',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Opportunistic sweep before the size check — if we're at the cap
|
|
83
|
+
// because the sweep timer hasn't run yet, expired entries should
|
|
84
|
+
// free up first rather than turning every retry into a 503.
|
|
85
|
+
sweepExpired();
|
|
86
|
+
if (pendingPairings.size >= exports.MAX_PENDING_PAIRINGS) {
|
|
87
|
+
log('pair.mint-rejected', `reason=cap-exceeded pending=${pendingPairings.size}`);
|
|
88
|
+
return (0, responses_1.errorResponse)(res, 503, {
|
|
89
|
+
error: 'pair-pending-cap',
|
|
90
|
+
hint: 'too many pending pairings — wait for the 5-min TTL to expire and retry',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const token = mintPairToken();
|
|
94
|
+
const expiresAt = Date.now() + exports.PAIR_TTL_MS;
|
|
95
|
+
pendingPairings.set(token, { bearer, expiresAt });
|
|
96
|
+
scheduleSweep();
|
|
97
|
+
log('pair.minted', `tokenPrefix=${prefix(token)}`, `expiresAt=${expiresAt}`);
|
|
98
|
+
// No `Cache-Control` override — `jsonResponse` defaults to `no-store`.
|
|
99
|
+
(0, responses_1.jsonResponse)(res, 201, { token, expiresAt });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* GET `/dashboard/api/pair/:token` — exchange the token for the bearer.
|
|
103
|
+
*
|
|
104
|
+
* Single-use: even if expired or never minted, return 410 Gone so an
|
|
105
|
+
* attacker can't distinguish "token never existed" from "token
|
|
106
|
+
* already consumed". The `error` field stays generic
|
|
107
|
+
* (`pair-token-invalid`) for the same reason.
|
|
108
|
+
*/
|
|
109
|
+
function handlePairConsume(_req, res, token) {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const entry = pendingPairings.get(token);
|
|
112
|
+
if (!entry || entry.expiresAt <= now) {
|
|
113
|
+
if (entry)
|
|
114
|
+
pendingPairings.delete(token); // expired — clean up
|
|
115
|
+
log('pair.consume-rejected', `tokenPrefix=${prefix(token)}`, `reason=invalid-or-expired`);
|
|
116
|
+
return (0, responses_1.errorResponse)(res, 410, { error: 'pair-token-invalid' });
|
|
117
|
+
}
|
|
118
|
+
// Single-use: delete IMMEDIATELY before we even write the response
|
|
119
|
+
// so a duplicate request lands on the "absent" branch above.
|
|
120
|
+
pendingPairings.delete(token);
|
|
121
|
+
log('pair.consumed', `tokenPrefix=${prefix(token)}`);
|
|
122
|
+
(0, responses_1.jsonResponse)(res, 200, {
|
|
123
|
+
bearer: entry.bearer,
|
|
124
|
+
expiresAt: entry.expiresAt,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Test escape hatch — wipe the in-memory map so each test starts
|
|
129
|
+
* from zero pendingPairings. Production code MUST NOT call this.
|
|
130
|
+
*
|
|
131
|
+
* Following the project's `__<verb><Noun>ForTests` naming convention
|
|
132
|
+
* (ADR 0006). Also stops the sweep timer so vitest exits cleanly.
|
|
133
|
+
*/
|
|
134
|
+
function __resetPairingsForTests() {
|
|
135
|
+
pendingPairings.clear();
|
|
136
|
+
if (sweepTimer) {
|
|
137
|
+
clearInterval(sweepTimer);
|
|
138
|
+
sweepTimer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Test-only — peek at the current count of pending pairings. */
|
|
142
|
+
function __pendingPairingsCountForTests() {
|
|
143
|
+
return pendingPairings.size;
|
|
144
|
+
}
|
|
145
|
+
/** Test-only — manually trigger the sweep at a synthetic `now`. */
|
|
146
|
+
function __sweepExpiredForTests(now) {
|
|
147
|
+
return sweepExpired(now);
|
|
148
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
/** Repo-relative path from the compiled `dist/` output — the production default. */
|
|
3
|
+
export declare const DASHBOARD_DIST: string;
|
|
4
|
+
/**
|
|
5
|
+
* Serve a request under `/dashboard/*`. The caller in `server.ts` has
|
|
6
|
+
* already URL-parsed the request and gated by prefix, and passes the
|
|
7
|
+
* normalised `pathname` in to avoid a second parse on the hot path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleDashboardStatic(req: IncomingMessage, res: ServerResponse, pathname: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Test escape hatch — override the dist root used by
|
|
12
|
+
* {@link handleDashboardStatic}. Pass `null` to restore the production
|
|
13
|
+
* default.
|
|
14
|
+
*
|
|
15
|
+
* **Do not call from production code.** The double-underscore prefix
|
|
16
|
+
* marks this as a test-only hook (see ADR 0006). Used by
|
|
17
|
+
* `tests/http/dashboard-handler.test.ts` to point the handler at a
|
|
18
|
+
* fixture directory without modifying the real `dashboard/dist/`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function __setDashboardDistRootForTests(override: string | null): void;
|