@songsid/agend 2.3.4-beta.7 → 2.3.5-alpha.3
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 +2 -0
- package/README.zh-TW.md +2 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/dist/agent-endpoint.d.ts +5 -1
- package/dist/agent-endpoint.js +12 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +21 -0
- package/dist/auth-api.js +89 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/web-authority.d.ts +12 -2
- package/dist/authority/web-authority.js +25 -4
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/claude-code.d.ts +15 -12
- package/dist/backend/claude-code.js +152 -133
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-package.d.ts +5 -2
- package/dist/backend/codex-package.js +1 -5
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex.d.ts +136 -113
- package/dist/backend/codex.js +1281 -1219
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok-model-cache.d.ts +7 -0
- package/dist/backend/grok-model-cache.js +65 -0
- package/dist/backend/grok-model-cache.js.map +1 -0
- package/dist/backend/grok.d.ts +2 -0
- package/dist/backend/grok.js +22 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/native-pane.d.ts +9 -0
- package/dist/backend/native-pane.js +59 -0
- package/dist/backend/native-pane.js.map +1 -0
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +2 -0
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +71 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +67 -5
- package/dist/daemon.js +823 -266
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +15 -3
- package/dist/delivery-consumed-watch.js +23 -1
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.d.ts +5 -0
- package/dist/delivery-outbox.js +32 -4
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +2 -35
- package/dist/delivery-reconciliation.js +2 -156
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -2
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +238 -3
- package/dist/fleet-manager.js +1826 -104
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-windows-runtime.d.ts +92 -0
- package/dist/fleet-windows-runtime.js +380 -0
- package/dist/fleet-windows-runtime.js.map +1 -0
- package/dist/general-binding.d.ts +85 -0
- package/dist/general-binding.js +298 -0
- package/dist/general-binding.js.map +1 -0
- package/dist/general-host-owner.d.ts +8 -0
- package/dist/general-host-owner.js +26 -0
- package/dist/general-host-owner.js.map +1 -0
- package/dist/general-instance.d.ts +7 -0
- package/dist/general-instance.js +20 -3
- package/dist/general-instance.js.map +1 -1
- package/dist/general-role.d.ts +5 -0
- package/dist/general-role.js +26 -0
- package/dist/general-role.js.map +1 -0
- package/dist/instance-config-impact.js +4 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +21 -3
- package/dist/instance-lifecycle.js +378 -162
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +6 -0
- package/dist/instructions.js.map +1 -1
- package/dist/local-pane-endpoint.d.ts +46 -0
- package/dist/local-pane-endpoint.js +189 -0
- package/dist/local-pane-endpoint.js.map +1 -0
- package/dist/local-pane-launcher.d.ts +2 -0
- package/dist/local-pane-launcher.js +38 -0
- package/dist/local-pane-launcher.js.map +1 -0
- package/dist/local-pane-stage.ps1 +101 -0
- package/dist/locale.js +85 -0
- package/dist/locale.js.map +1 -1
- package/dist/model-catalog-defaults.d.ts +14 -0
- package/dist/model-catalog-defaults.js +20 -0
- package/dist/model-catalog-defaults.js.map +1 -0
- package/dist/native-agent-admission.d.ts +7 -0
- package/dist/native-agent-admission.js +26 -0
- package/dist/native-agent-admission.js.map +1 -0
- package/dist/native-daemon-client.d.ts +13 -0
- package/dist/native-daemon-client.js +28 -0
- package/dist/native-daemon-client.js.map +1 -0
- package/dist/native-instance-api.d.ts +14 -0
- package/dist/native-instance-api.js +205 -0
- package/dist/native-instance-api.js.map +1 -0
- package/dist/native-instance-input.d.ts +84 -0
- package/dist/native-instance-input.js +54 -0
- package/dist/native-instance-input.js.map +1 -0
- package/dist/native-instance-registry.d.ts +32 -0
- package/dist/native-instance-registry.js +105 -0
- package/dist/native-instance-registry.js.map +1 -0
- package/dist/native-mcp.d.ts +40 -0
- package/dist/native-mcp.js +278 -0
- package/dist/native-mcp.js.map +1 -0
- package/dist/native-model-catalog.d.ts +5 -0
- package/dist/native-model-catalog.js +76 -0
- package/dist/native-model-catalog.js.map +1 -0
- package/dist/native-pane-crash-budget.d.ts +20 -0
- package/dist/native-pane-crash-budget.js +60 -0
- package/dist/native-pane-crash-budget.js.map +1 -0
- package/dist/native-pane-create.d.ts +26 -0
- package/dist/native-pane-create.js +53 -0
- package/dist/native-pane-create.js.map +1 -0
- package/dist/native-pane-lifecycle.d.ts +65 -0
- package/dist/native-pane-lifecycle.js +230 -0
- package/dist/native-pane-lifecycle.js.map +1 -0
- package/dist/native-pane-manager.d.ts +59 -0
- package/dist/native-pane-manager.js +584 -0
- package/dist/native-pane-manager.js.map +1 -0
- package/dist/native-pane-output-log.d.ts +15 -0
- package/dist/native-pane-output-log.js +108 -0
- package/dist/native-pane-output-log.js.map +1 -0
- package/dist/native-pane-reconciliation.d.ts +4 -0
- package/dist/native-pane-reconciliation.js +40 -0
- package/dist/native-pane-reconciliation.js.map +1 -0
- package/dist/native-pane-recovery.d.ts +12 -0
- package/dist/native-pane-recovery.js +10 -0
- package/dist/native-pane-recovery.js.map +1 -0
- package/dist/native-pane-tools.d.ts +2 -0
- package/dist/native-pane-tools.js +14 -0
- package/dist/native-pane-tools.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -0
- package/dist/needs-you-hub.js +2 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +19 -1
- package/dist/needs-you.js +12 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +16 -2
- package/dist/outbound-handlers.js +67 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +38 -0
- package/dist/outbound-schemas.js +12 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-port.d.ts +118 -0
- package/dist/pane-port.js +2 -0
- package/dist/pane-port.js.map +1 -0
- package/dist/public-web-gateway.js +9 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +5 -0
- package/dist/public-web-link.js +4 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js +9 -5
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +4 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/runtime-api.d.ts +8 -0
- package/dist/runtime-api.js +96 -0
- package/dist/runtime-api.js.map +1 -0
- package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
- package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
- package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/dist/runtime-assets/@xterm/headless/package.json +29 -0
- package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
- package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
- package/dist/runtime-assets/node-pty/LICENSE +69 -0
- package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/runtime-assets/node-pty/lib/index.js +52 -0
- package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
- package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
- package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
- package/dist/runtime-assets/node-pty/lib/types.js +7 -0
- package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
- package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
- package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
- package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
- package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
- package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/runtime-assets/node-pty/package.json +64 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/dist/runtime-bundle.d.ts +17 -0
- package/dist/runtime-bundle.js +150 -0
- package/dist/runtime-bundle.js.map +1 -0
- package/dist/runtime-cli.d.ts +2 -0
- package/dist/runtime-cli.js +117 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-client-store.d.ts +49 -0
- package/dist/runtime-client-store.js +292 -0
- package/dist/runtime-client-store.js.map +1 -0
- package/dist/runtime-client.d.ts +100 -0
- package/dist/runtime-client.js +700 -0
- package/dist/runtime-client.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +82 -0
- package/dist/runtime-coordinator.js +864 -0
- package/dist/runtime-coordinator.js.map +1 -0
- package/dist/runtime-general-protocol.d.ts +22 -0
- package/dist/runtime-general-protocol.js +57 -0
- package/dist/runtime-general-protocol.js.map +1 -0
- package/dist/runtime-general-workspace.d.ts +17 -0
- package/dist/runtime-general-workspace.js +213 -0
- package/dist/runtime-general-workspace.js.map +1 -0
- package/dist/runtime-pane-assets.d.ts +17 -0
- package/dist/runtime-pane-assets.js +23 -0
- package/dist/runtime-pane-assets.js.map +1 -0
- package/dist/runtime-pane-boot.d.ts +2 -0
- package/dist/runtime-pane-boot.js +24 -0
- package/dist/runtime-pane-boot.js.map +1 -0
- package/dist/runtime-pane-codex-index.d.ts +1 -0
- package/dist/runtime-pane-codex-index.js +47 -0
- package/dist/runtime-pane-codex-index.js.map +1 -0
- package/dist/runtime-pane-config.d.ts +16 -0
- package/dist/runtime-pane-config.js +54 -0
- package/dist/runtime-pane-config.js.map +1 -0
- package/dist/runtime-pane-host.d.ts +44 -0
- package/dist/runtime-pane-host.js +424 -0
- package/dist/runtime-pane-host.js.map +1 -0
- package/dist/runtime-pane-job-owner.d.ts +1 -0
- package/dist/runtime-pane-job-owner.js +29 -0
- package/dist/runtime-pane-job-owner.js.map +1 -0
- package/dist/runtime-pane-job.ps1 +116 -0
- package/dist/runtime-pane-link.d.ts +40 -0
- package/dist/runtime-pane-link.js +246 -0
- package/dist/runtime-pane-link.js.map +1 -0
- package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
- package/dist/runtime-pane-mcp-protocol.js +39 -0
- package/dist/runtime-pane-mcp-protocol.js.map +1 -0
- package/dist/runtime-pane-mcp-relay.d.ts +17 -0
- package/dist/runtime-pane-mcp-relay.js +70 -0
- package/dist/runtime-pane-mcp-relay.js.map +1 -0
- package/dist/runtime-pane-mcp.d.ts +51 -0
- package/dist/runtime-pane-mcp.js +150 -0
- package/dist/runtime-pane-mcp.js.map +1 -0
- package/dist/runtime-pane-native.d.ts +45 -0
- package/dist/runtime-pane-native.js +65 -0
- package/dist/runtime-pane-native.js.map +1 -0
- package/dist/runtime-pane-output.d.ts +19 -0
- package/dist/runtime-pane-output.js +53 -0
- package/dist/runtime-pane-output.js.map +1 -0
- package/dist/runtime-pane-port.d.ts +139 -0
- package/dist/runtime-pane-port.js +558 -0
- package/dist/runtime-pane-port.js.map +1 -0
- package/dist/runtime-pane-private.d.ts +14 -0
- package/dist/runtime-pane-private.js +51 -0
- package/dist/runtime-pane-private.js.map +1 -0
- package/dist/runtime-pane-private.ps1 +14 -0
- package/dist/runtime-pane-process.d.ts +4 -0
- package/dist/runtime-pane-process.js +108 -0
- package/dist/runtime-pane-process.js.map +1 -0
- package/dist/runtime-pane-protocol.d.ts +172 -0
- package/dist/runtime-pane-protocol.js +195 -0
- package/dist/runtime-pane-protocol.js.map +1 -0
- package/dist/runtime-pane-screen.d.ts +19 -0
- package/dist/runtime-pane-screen.js +76 -0
- package/dist/runtime-pane-screen.js.map +1 -0
- package/dist/runtime-pane-service.d.ts +11 -0
- package/dist/runtime-pane-service.js +221 -0
- package/dist/runtime-pane-service.js.map +1 -0
- package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
- package/dist/runtime-pane-stdio-protocol.js +94 -0
- package/dist/runtime-pane-stdio-protocol.js.map +1 -0
- package/dist/runtime-pane-stdio.d.ts +3 -0
- package/dist/runtime-pane-stdio.js +123 -0
- package/dist/runtime-pane-stdio.js.map +1 -0
- package/dist/runtime-pane-store.d.ts +33 -0
- package/dist/runtime-pane-store.js +164 -0
- package/dist/runtime-pane-store.js.map +1 -0
- package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
- package/dist/runtime-pane-transcript-protocol.js +58 -0
- package/dist/runtime-pane-transcript-protocol.js.map +1 -0
- package/dist/runtime-pane-transcript.d.ts +45 -0
- package/dist/runtime-pane-transcript.js +605 -0
- package/dist/runtime-pane-transcript.js.map +1 -0
- package/dist/runtime-pane-worker.d.ts +1 -0
- package/dist/runtime-pane-worker.js +154 -0
- package/dist/runtime-pane-worker.js.map +1 -0
- package/dist/runtime-protocol.d.ts +149 -0
- package/dist/runtime-protocol.js +19 -0
- package/dist/runtime-protocol.js.map +1 -0
- package/dist/runtime-switches.d.ts +79 -0
- package/dist/runtime-switches.js +90 -0
- package/dist/runtime-switches.js.map +1 -0
- package/dist/runtime-transcript-delta-reader.d.ts +23 -0
- package/dist/runtime-transcript-delta-reader.js +74 -0
- package/dist/runtime-transcript-delta-reader.js.map +1 -0
- package/dist/runtime-transcript-source.d.ts +36 -0
- package/dist/runtime-transcript-source.js +189 -0
- package/dist/runtime-transcript-source.js.map +1 -0
- package/dist/runtime-windows-bridge.d.ts +41 -0
- package/dist/runtime-windows-bridge.js +157 -0
- package/dist/runtime-windows-bridge.js.map +1 -0
- package/dist/settings-api.d.ts +5 -0
- package/dist/settings-api.js +95 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +1 -0
- package/dist/settings-baseline.js +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +28 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +16 -1
- package/dist/settings-confirmation.js +47 -9
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.d.ts +4 -0
- package/dist/settings-effect.js +58 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +1 -0
- package/dist/settings-http-confirmation.js +15 -2
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +2 -0
- package/dist/settings-request-capability.js +4 -0
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/settings-route-lifetime.d.ts +14 -0
- package/dist/settings-route-lifetime.js +48 -0
- package/dist/settings-route-lifetime.js.map +1 -0
- package/dist/settings-transaction.d.ts +4 -0
- package/dist/settings-transaction.js +9 -0
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-form.js +6 -3
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +3 -0
- package/dist/setup-i18n.js.map +1 -1
- package/dist/tmux-control.d.ts +2 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-pane-host.d.ts +26 -0
- package/dist/tmux-pane-host.js +25 -0
- package/dist/tmux-pane-host.js.map +1 -0
- package/dist/tool-permissions.js +2 -1
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +3 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +1 -1
- package/dist/transcript-delta-reader.js +1 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +3 -0
- package/dist/transcript-monitor.js +22 -46
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-needles.d.ts +35 -0
- package/dist/transcript-needles.js +158 -0
- package/dist/transcript-needles.js.map +1 -0
- package/dist/transcript-records.d.ts +21 -0
- package/dist/transcript-records.js +140 -0
- package/dist/transcript-records.js.map +1 -0
- package/dist/transcript-sources.d.ts +5 -0
- package/dist/transcript-sources.js +8 -79
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/ui/agent-proposals.js +90 -0
- package/dist/ui/agent-runtime.js +106 -0
- package/dist/ui/chat-store.js +13 -2
- package/dist/ui/chat-strings.js +48 -0
- package/dist/ui/cli-picker.js +42 -0
- package/dist/ui/general-location.js +71 -0
- package/dist/ui/native-instance-settings.js +47 -0
- package/dist/ui/native-pane-recovery.js +45 -0
- package/dist/ui/panel-chat.js +63 -32
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +99 -18
- package/dist/ui/panel-needs.js +154 -3
- package/dist/ui/panel-runtimes.js +231 -0
- package/dist/ui/panel-settings.js +19 -9
- package/dist/ui/panel-windows-agent.js +86 -0
- package/dist/ui/panel-windows-cli.js +143 -0
- package/dist/ui/provider-marks.js +83 -0
- package/dist/ui/runtime-catalog.js +55 -0
- package/dist/ui/settings-catalog.js +37 -10
- package/dist/ui/settings-confirm.js +3 -1
- package/dist/ui/settings-dialogs.js +22 -7
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-outside.js +176 -0
- package/dist/ui/settings-registry.js +6 -0
- package/dist/ui/settings-strings.js +16 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +6 -2
- package/dist/ui/shared/app-shell.js +3 -2
- package/dist/ui/shared/app-store.js +1 -0
- package/dist/ui/shared/app-stream.js +3 -1
- package/dist/ui/shared/app.css +301 -0
- package/dist/ui/shared/app.js +4 -0
- package/dist/ui/shared/header-tools.js +6 -4
- package/dist/ui/shared/instance-nav.js +33 -7
- package/dist/ui/shared/instance-switch.js +4 -3
- package/dist/ui/shared/qrcode.module.js +2237 -0
- package/dist/ui/shared/signin.js +39 -5
- package/dist/ui/shared/ui-menu.js +28 -8
- package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
- package/dist/ui/shared/vendor/vendor.json +19 -2
- package/dist/ui/shared/view-strings.js +8 -0
- package/dist/ui/windows-agents-nav.js +2 -0
- package/dist/ui/windows-cli-catalog.js +79 -0
- package/dist/web-api.d.ts +37 -0
- package/dist/web-api.js +248 -6
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +19 -2
- package/dist/web-chat-history.js +72 -3
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-only-fleet.d.ts +20 -0
- package/dist/web-only-fleet.js +21 -0
- package/dist/web-only-fleet.js.map +1 -1
- package/dist/web-remote-access.d.ts +152 -0
- package/dist/web-remote-access.js +368 -0
- package/dist/web-remote-access.js.map +1 -0
- package/dist/web-session.d.ts +7 -0
- package/dist/web-session.js +2 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -1
- package/dist/web-shell-routes.js +7 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/windows-agents.d.ts +124 -0
- package/dist/windows-agents.js +487 -0
- package/dist/windows-agents.js.map +1 -0
- package/dist/windows-cli-api.d.ts +23 -0
- package/dist/windows-cli-api.js +158 -0
- package/dist/windows-cli-api.js.map +1 -0
- package/dist/windows-cli-providers.d.ts +115 -0
- package/dist/windows-cli-providers.js +429 -0
- package/dist/windows-cli-providers.js.map +1 -0
- package/dist/windows-cli-worker.d.ts +58 -0
- package/dist/windows-cli-worker.js +479 -0
- package/dist/windows-cli-worker.js.map +1 -0
- package/dist/windows-cli.d.ts +115 -0
- package/dist/windows-cli.js +402 -0
- package/dist/windows-cli.js.map +1 -0
- package/dist/windows-native-mcp.d.ts +19 -0
- package/dist/windows-native-mcp.js +143 -0
- package/dist/windows-native-mcp.js.map +1 -0
- package/dist/windows-pane-mcp.d.ts +26 -0
- package/dist/windows-pane-mcp.js +187 -0
- package/dist/windows-pane-mcp.js.map +1 -0
- package/package.json +9 -3
- package/templates/runtime-assets/conpty-LICENSE +21 -0
- package/templates/runtime-assets/conpty-NOTICE.md +503 -0
- package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
- package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
- package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
- package/templates/runtime-assets/xterm-LICENSE +21 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
/** Durable machine registration and outbound-client transport; never starts a remote shell. */
|
|
2
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
6
|
+
import { readBoundedWebBody } from "./web-body.js";
|
|
7
|
+
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
8
|
+
import { validateNativeTask } from "./windows-cli-worker.js";
|
|
9
|
+
import { inventoryProbed } from "./windows-cli.js";
|
|
10
|
+
import { isNativeProviderId } from "./windows-cli-providers.js";
|
|
11
|
+
import { validatePaneMcpBody } from "./runtime-pane-mcp-protocol.js";
|
|
12
|
+
import { validateRuntimePaneRequest, validateRuntimePaneSnapshot, validateRuntimePaneShutdown } from "./runtime-pane-protocol.js";
|
|
13
|
+
import { RUNTIME_CLAIM_PATH, RUNTIME_CONNECT_PATH, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PAIR_PATH, RUNTIME_ROTATE_PATH, runtimeFingerprintFromHash } from "./runtime-protocol.js";
|
|
14
|
+
const PAIR_TTL = 10 * 60_000;
|
|
15
|
+
const MAX_STORE_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
17
|
+
const secret = () => randomBytes(32).toString("hex");
|
|
18
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
19
|
+
const equal = (value, digest) => /^[a-f0-9]{64}$/i.test(digest) && timingSafeEqual(Buffer.from(hash(value), "hex"), Buffer.from(digest, "hex"));
|
|
20
|
+
const record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
21
|
+
const text = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max && !/[\0\r\n]/.test(v);
|
|
22
|
+
const terminal = (task) => !!task.finishedAt && ["completed", "failed", "cancelled"].includes(task.status);
|
|
23
|
+
const loopback = (address) => address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
24
|
+
function privateTransport(req) {
|
|
25
|
+
return req.headers.origin === undefined && req.headers.cookie === undefined
|
|
26
|
+
&& (loopback(req.socket.remoteAddress) || req.socket.encrypted === true);
|
|
27
|
+
}
|
|
28
|
+
function stable(value, depth = 0) {
|
|
29
|
+
if (depth > 64)
|
|
30
|
+
throw Error("Runtime request is too deeply nested");
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return `[${value.map(value => stable(value, depth + 1)).join(",")}]`;
|
|
33
|
+
if (record(value))
|
|
34
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key], depth + 1)}`).join(",")}}`;
|
|
35
|
+
return JSON.stringify(value) ?? "null";
|
|
36
|
+
}
|
|
37
|
+
function inventory(value) {
|
|
38
|
+
if (!record(value) || typeof value.home !== "string" || value.home.length > 1024 || !Array.isArray(value.clis) || value.clis.length > 16)
|
|
39
|
+
throw Error("Invalid runtime inventory");
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const row of value.clis) {
|
|
42
|
+
if (!record(row) || !isNativeProviderId(row.id) || seen.has(row.id) || !record(row.supports)
|
|
43
|
+
|| typeof row.runnable !== "boolean" || typeof row.readOnlySupport !== "boolean"
|
|
44
|
+
|| ![true, false, null].includes(row.authenticated)
|
|
45
|
+
|| !(row.path === null || text(row.path, 2048)) || !(row.version === null || text(row.version, 256))
|
|
46
|
+
|| typeof row.permissionNote !== "string" || row.permissionNote.length > 2048
|
|
47
|
+
|| !Array.isArray(row.models) || row.models.length > 256)
|
|
48
|
+
throw Error("Invalid runtime CLI inventory");
|
|
49
|
+
for (const model of row.models)
|
|
50
|
+
if (!record(model) || !text(model.id, 256) || model.name !== undefined && !text(model.name, 256)
|
|
51
|
+
|| model.description !== undefined && (typeof model.description !== "string" || model.description.length > 2048))
|
|
52
|
+
throw Error("Invalid runtime model inventory");
|
|
53
|
+
if (Object.values(row.supports).some(v => typeof v !== "boolean"))
|
|
54
|
+
throw Error("Invalid runtime capabilities");
|
|
55
|
+
seen.add(row.id);
|
|
56
|
+
}
|
|
57
|
+
if (Buffer.byteLength(JSON.stringify(value)) > RUNTIME_MAX_FRAME_BYTES / 2)
|
|
58
|
+
throw Error("Runtime inventory is too large");
|
|
59
|
+
return structuredClone(value);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* M-2239 P2-1 (#2936 review): the journal is compacted, never just capped. A settled command (failed, or accepted with a
|
|
63
|
+
* terminal, process-confirmed outcome) older than COMMAND_KEEP_MS, or the oldest settled ones beyond COMMAND_SOFT_CAP,
|
|
64
|
+
* become tombstones; pending and uncertain commands are never compacted. Tombstones are evicted only beyond
|
|
65
|
+
* TOMBSTONE_CAP and only when older than TOMBSTONE_KEEP_MS — a replayed id is refused for at least that long.
|
|
66
|
+
*/
|
|
67
|
+
const COMMAND_HARD_CAP = 4096, COMMAND_SOFT_CAP = 2048, COMMAND_KEEP_MS = 7 * 24 * 3_600_000;
|
|
68
|
+
const TOMBSTONE_CAP = 65_536, TOMBSTONE_KEEP_MS = 30 * 24 * 3_600_000;
|
|
69
|
+
/**
|
|
70
|
+
* alpha.2 polish: what an interrupted command becomes. A probe only reads the runner's CLI inventory — nothing to reconcile
|
|
71
|
+
* if its answer was lost — so it settles as failed (compacted like any settled command); every other operation stays
|
|
72
|
+
* uncertain (never compacted, never sent again). The automatic probe on connect must not leave a trail in the journal.
|
|
73
|
+
*/
|
|
74
|
+
const interrupted = (command) => command.operation === "probe" ? "failed" : "uncertain";
|
|
75
|
+
export class RuntimeCoordinator {
|
|
76
|
+
options;
|
|
77
|
+
paneEpoch = randomUUID();
|
|
78
|
+
path;
|
|
79
|
+
state;
|
|
80
|
+
server = new WebSocketServer({ noServer: true, maxPayload: RUNTIME_MAX_FRAME_BYTES, perMessageDeflate: false });
|
|
81
|
+
connections = new Map();
|
|
82
|
+
pending = new Map();
|
|
83
|
+
paneToolPending = new Set();
|
|
84
|
+
panePending = new Map();
|
|
85
|
+
listeners = new Set();
|
|
86
|
+
limits = new Map();
|
|
87
|
+
heartbeat;
|
|
88
|
+
closed = false;
|
|
89
|
+
constructor(home, options = {}) {
|
|
90
|
+
this.options = options;
|
|
91
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
92
|
+
this.path = join(home, "runtime-machines.json");
|
|
93
|
+
if (existsSync(this.path)) {
|
|
94
|
+
const stat = lstatSync(this.path);
|
|
95
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_STORE_BYTES)
|
|
96
|
+
throw Error("Invalid runtime machine store file");
|
|
97
|
+
}
|
|
98
|
+
this.state = existsSync(this.path) ? JSON.parse(readFileSync(this.path, "utf8")) : { version: 1, machines: [], pairings: [], commands: [] };
|
|
99
|
+
if (this.state.version !== 1 || !Array.isArray(this.state.machines) || !Array.isArray(this.state.pairings) || !Array.isArray(this.state.commands)
|
|
100
|
+
|| this.state.tombstones !== undefined && !Array.isArray(this.state.tombstones))
|
|
101
|
+
throw Error("Invalid runtime machine store");
|
|
102
|
+
this.state.tombstones ??= [];
|
|
103
|
+
if (this.state.machines.length > 256 || this.state.pairings.length > 16 || this.state.commands.length > COMMAND_HARD_CAP || this.state.tombstones.length > TOMBSTONE_CAP
|
|
104
|
+
|| this.state.tombstones.some(row => !record(row) || !text(row.id, 128) || !UUID.test(String(row.machineId)) || !/^[a-f0-9]{64}$/.test(row.digest) || !Number.isFinite(row.at))
|
|
105
|
+
|| this.state.machines.some(row => !record(row) || !record(row.machine) || !UUID.test(String(row.machine.id)) || !text(row.machine.name, 100)
|
|
106
|
+
|| row.machine.platform !== "win32" || !["pending", "offline", "online", "revoked"].includes(String(row.machine.status))
|
|
107
|
+
|| !Number.isSafeInteger(row.machine.generation) || row.machine.generation < 0
|
|
108
|
+
|| row.machine.pairingFingerprint !== undefined && (typeof row.machine.pairingFingerprint !== "string" || !/^[A-F0-9]{4}(?:-[A-F0-9]{4}){2}$/.test(row.machine.pairingFingerprint))
|
|
109
|
+
|| row.credentialHash !== undefined && !/^[a-f0-9]{64}$/.test(row.credentialHash)
|
|
110
|
+
|| row.claimHash !== undefined && !/^[a-f0-9]{64}$/.test(row.claimHash))
|
|
111
|
+
|| new Set(this.state.machines.map(row => row.machine.id)).size !== this.state.machines.length
|
|
112
|
+
|| this.state.pairings.some(row => !record(row) || !UUID.test(String(row.id)) || !/^[a-f0-9]{64}$/.test(row.codeHash) || !Number.isFinite(row.expiresAt))
|
|
113
|
+
|| this.state.commands.some(row => !record(row) || !text(row.id, 128) || !UUID.test(String(row.machineId)) || !/^[a-f0-9]{64}$/.test(row.digest)
|
|
114
|
+
|| !["probe", "workspace", "run", "cancel", "reconcile", "recover"].includes(row.operation)
|
|
115
|
+
|| !["pending", "accepted", "failed", "uncertain"].includes(row.state) || !Number.isSafeInteger(row.seq) || row.seq < 0))
|
|
116
|
+
throw Error("Malformed runtime machine store");
|
|
117
|
+
// A coordinator restart never grants execution authority or redelivers an in-flight command.
|
|
118
|
+
for (const row of this.state.machines) {
|
|
119
|
+
if (row.machine.status === "online")
|
|
120
|
+
row.machine.status = "offline";
|
|
121
|
+
if (row.claimHash)
|
|
122
|
+
row.machine.pairingFingerprint = runtimeFingerprintFromHash(row.claimHash);
|
|
123
|
+
}
|
|
124
|
+
for (const command of this.state.commands)
|
|
125
|
+
if (command.state === "pending")
|
|
126
|
+
command.state = interrupted(command);
|
|
127
|
+
this.save();
|
|
128
|
+
this.heartbeat = setInterval(() => {
|
|
129
|
+
for (const connection of this.connections.values()) {
|
|
130
|
+
if (!connection.alive) {
|
|
131
|
+
connection.ws.terminate();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
connection.alive = false;
|
|
135
|
+
connection.ws.ping();
|
|
136
|
+
}
|
|
137
|
+
}, 15_000);
|
|
138
|
+
this.heartbeat.unref();
|
|
139
|
+
}
|
|
140
|
+
now() { return this.options.now?.() ?? Date.now(); }
|
|
141
|
+
save() {
|
|
142
|
+
const value = JSON.stringify(this.state);
|
|
143
|
+
if (Buffer.byteLength(value) > MAX_STORE_BYTES)
|
|
144
|
+
throw Error("Runtime machine journal limit reached");
|
|
145
|
+
for (let attempt = 0;; attempt++) {
|
|
146
|
+
try {
|
|
147
|
+
atomicWriteFileSync(this.path, value, { mode: 0o600 });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// Windows scanners can briefly hold a destination open; retain atomic replacement and a bounded retry.
|
|
152
|
+
if (process.platform !== "win32" || attempt >= 4 || !["EPERM", "EACCES", "EBUSY"].includes(error.code ?? ""))
|
|
153
|
+
throw error;
|
|
154
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * (attempt + 1));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
emit(event) {
|
|
159
|
+
try {
|
|
160
|
+
this.options.changed?.(event);
|
|
161
|
+
}
|
|
162
|
+
catch { /* UI observers cannot change the durable verdict. */ }
|
|
163
|
+
for (const listener of this.listeners) {
|
|
164
|
+
try {
|
|
165
|
+
listener(structuredClone(event));
|
|
166
|
+
}
|
|
167
|
+
catch { /* Observer isolation. */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
entry(id) { const row = this.state.machines.find(v => v.machine.id === id); if (!row)
|
|
171
|
+
throw Error("Runtime machine not found"); return row; }
|
|
172
|
+
listMachines() { return this.state.machines.map(row => structuredClone(row.machine)); }
|
|
173
|
+
machine(id) { const row = this.state.machines.find(v => v.machine.id === id); return row ? structuredClone(row.machine) : null; }
|
|
174
|
+
subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
|
|
175
|
+
createPairing() {
|
|
176
|
+
this.state.pairings = this.state.pairings.filter(row => row.expiresAt > this.now());
|
|
177
|
+
if (this.state.pairings.length >= 16)
|
|
178
|
+
throw Error("Too many pending runtime pairing codes");
|
|
179
|
+
const code = randomBytes(6).toString("hex").toUpperCase(), pairingId = randomUUID(), expiresAt = this.now() + PAIR_TTL;
|
|
180
|
+
this.state.pairings.push({ id: pairingId, codeHash: hash(code), expiresAt });
|
|
181
|
+
this.save();
|
|
182
|
+
return { pairingId, code: code.match(/.{4}/g).join("-"), expiresAt };
|
|
183
|
+
}
|
|
184
|
+
pair(input) {
|
|
185
|
+
if (!record(input) || Object.keys(input).some(k => !["code", "name", "platform", "hostname"].includes(k))
|
|
186
|
+
|| !text(input.code, 32) || !text(input.name, 100) || !input.name.trim() || input.platform !== "win32"
|
|
187
|
+
|| input.hostname !== undefined && !text(input.hostname, 255))
|
|
188
|
+
throw Error("Invalid runtime pairing request");
|
|
189
|
+
if (this.state.machines.length >= 256 || this.state.machines.filter(row => row.machine.status !== "revoked").length >= 64)
|
|
190
|
+
throw Error("Runtime machine limit reached");
|
|
191
|
+
const code = input.code.replaceAll("-", "").toUpperCase();
|
|
192
|
+
const pairing = this.state.pairings.find(row => row.expiresAt > this.now() && equal(code, row.codeHash));
|
|
193
|
+
if (!pairing)
|
|
194
|
+
throw Error("Pairing code is invalid, used or expired");
|
|
195
|
+
const machineId = randomUUID(), claimSecret = secret();
|
|
196
|
+
this.state.pairings = this.state.pairings.filter(row => row !== pairing);
|
|
197
|
+
this.state.machines.push({ machine: { id: machineId, name: input.name.trim(), platform: "win32", ...(input.hostname ? { hostname: input.hostname } : {}), status: "pending", createdAt: this.now(), generation: 0, pairingFingerprint: runtimeFingerprintFromHash(hash(claimSecret)) }, claimHash: hash(claimSecret), claimExpiresAt: pairing.expiresAt });
|
|
198
|
+
this.save();
|
|
199
|
+
this.emit({ type: "machine", machineId });
|
|
200
|
+
return { machineId, claimSecret, status: "pending", expiresAt: pairing.expiresAt };
|
|
201
|
+
}
|
|
202
|
+
claim(input) {
|
|
203
|
+
if (!record(input) || Object.keys(input).some(k => !["machineId", "claimSecret"].includes(k)) || !text(input.machineId, 64) || !text(input.claimSecret, 128))
|
|
204
|
+
throw Error("Invalid runtime claim");
|
|
205
|
+
const row = this.state.machines.find(row => row.machine.id === input.machineId);
|
|
206
|
+
if (!row?.claimHash || !row.claimExpiresAt || row.claimExpiresAt <= this.now() || !equal(input.claimSecret, row.claimHash) || row.machine.status === "revoked")
|
|
207
|
+
throw Error("Runtime claim is invalid or expired");
|
|
208
|
+
if (!row.machine.approvedAt)
|
|
209
|
+
return { status: "pending" };
|
|
210
|
+
const credential = secret();
|
|
211
|
+
row.credentialHash = hash(credential);
|
|
212
|
+
delete row.claimHash;
|
|
213
|
+
delete row.claimExpiresAt;
|
|
214
|
+
this.save();
|
|
215
|
+
return { machineId: row.machine.id, credential, status: "approved" };
|
|
216
|
+
}
|
|
217
|
+
approve(id, fingerprint) {
|
|
218
|
+
const row = this.entry(id);
|
|
219
|
+
if (row.machine.status !== "pending" || !row.claimExpiresAt || row.claimExpiresAt <= this.now())
|
|
220
|
+
throw Error("Runtime pairing is no longer pending");
|
|
221
|
+
if (!fingerprint || fingerprint !== row.machine.pairingFingerprint)
|
|
222
|
+
throw Error("Compare the pairing fingerprint shown by this Runtime before approving");
|
|
223
|
+
row.machine.approvedAt = this.now();
|
|
224
|
+
row.machine.status = "offline";
|
|
225
|
+
this.save();
|
|
226
|
+
this.emit({ type: "machine", machineId: id });
|
|
227
|
+
return structuredClone(row.machine);
|
|
228
|
+
}
|
|
229
|
+
rename(id, name) {
|
|
230
|
+
if (!text(name, 100) || !name.trim())
|
|
231
|
+
throw Error("Invalid runtime machine name");
|
|
232
|
+
const row = this.entry(id);
|
|
233
|
+
row.machine.name = name.trim();
|
|
234
|
+
this.save();
|
|
235
|
+
this.emit({ type: "machine", machineId: id });
|
|
236
|
+
return structuredClone(row.machine);
|
|
237
|
+
}
|
|
238
|
+
revoke(id) {
|
|
239
|
+
const row = this.entry(id);
|
|
240
|
+
row.machine.status = "revoked";
|
|
241
|
+
delete row.credentialHash;
|
|
242
|
+
delete row.claimHash;
|
|
243
|
+
delete row.claimExpiresAt;
|
|
244
|
+
this.save();
|
|
245
|
+
const connection = this.connections.get(id);
|
|
246
|
+
if (connection) {
|
|
247
|
+
this.connections.delete(id);
|
|
248
|
+
this.rejectPending(connection, "Runtime machine was revoked");
|
|
249
|
+
connection.ws.close(1008, "Machine revoked");
|
|
250
|
+
}
|
|
251
|
+
this.emit({ type: "machine", machineId: id });
|
|
252
|
+
return structuredClone(row.machine);
|
|
253
|
+
}
|
|
254
|
+
credentialOwner(auth) {
|
|
255
|
+
if (typeof auth !== "string" || !/^Bearer [a-f0-9]{64}$/i.test(auth))
|
|
256
|
+
return;
|
|
257
|
+
return this.state.machines.find(row => row.credentialHash && row.machine.status !== "revoked" && row.machine.approvedAt && equal(auth.slice(7), row.credentialHash));
|
|
258
|
+
}
|
|
259
|
+
/** Valid capabilities never share the invalid-traffic bucket of a loopback proxy. Forwarding headers are untrusted. */
|
|
260
|
+
rate(key, limit) {
|
|
261
|
+
const now = this.now();
|
|
262
|
+
for (const [id, value] of this.limits)
|
|
263
|
+
if (value.until <= now)
|
|
264
|
+
this.limits.delete(id);
|
|
265
|
+
if (!this.limits.has(key) && this.limits.size >= 512) {
|
|
266
|
+
const invalid = [...this.limits.keys()].find(id => id.startsWith("invalid:"));
|
|
267
|
+
if (!invalid || key.startsWith("invalid:"))
|
|
268
|
+
return false;
|
|
269
|
+
this.limits.delete(invalid);
|
|
270
|
+
}
|
|
271
|
+
const old = this.limits.get(key), row = old && old.until > now ? old : { count: 0, until: now + 60_000 };
|
|
272
|
+
this.limits.set(key, row);
|
|
273
|
+
return ++row.count <= limit;
|
|
274
|
+
}
|
|
275
|
+
requestRateKey(path, input, req) {
|
|
276
|
+
if (path === RUNTIME_ROTATE_PATH) {
|
|
277
|
+
const row = this.credentialOwner(req.headers.authorization);
|
|
278
|
+
if (row)
|
|
279
|
+
return `rotate:${row.machine.id}`;
|
|
280
|
+
}
|
|
281
|
+
else if (record(input)) {
|
|
282
|
+
if (path === RUNTIME_PAIR_PATH && text(input.code, 32)) {
|
|
283
|
+
const code = input.code.replaceAll("-", "").toUpperCase();
|
|
284
|
+
const row = this.state.pairings.find(row => row.expiresAt > this.now() && equal(code, row.codeHash));
|
|
285
|
+
if (row)
|
|
286
|
+
return `pair:${row.id}`;
|
|
287
|
+
}
|
|
288
|
+
if (path === RUNTIME_CLAIM_PATH && text(input.claimSecret, 128)) {
|
|
289
|
+
const row = this.state.machines.find(row => row.machine.id === input.machineId && row.claimHash && row.claimExpiresAt > this.now()
|
|
290
|
+
&& row.machine.status !== "revoked" && equal(input.claimSecret, row.claimHash));
|
|
291
|
+
if (row)
|
|
292
|
+
return `claim:${row.machine.id}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return `invalid:${req.socket.remoteAddress ?? "unknown"}`;
|
|
296
|
+
}
|
|
297
|
+
rotateCredential(auth, input) {
|
|
298
|
+
const row = this.credentialOwner(auth);
|
|
299
|
+
if (!row || !record(input) || Object.keys(input).some(key => !["machineId", "credential"].includes(key))
|
|
300
|
+
|| input.machineId !== row.machine.id || typeof input.credential !== "string" || !/^[a-f0-9]{64}$/.test(input.credential))
|
|
301
|
+
throw Error("Invalid runtime credential rotation");
|
|
302
|
+
if (!equal(input.credential, row.credentialHash)) {
|
|
303
|
+
const before = { credentialHash: row.credentialHash, status: row.machine.status };
|
|
304
|
+
row.credentialHash = hash(input.credential);
|
|
305
|
+
row.machine.status = "offline";
|
|
306
|
+
try {
|
|
307
|
+
this.save();
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
row.credentialHash = before.credentialHash;
|
|
311
|
+
row.machine.status = before.status;
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
const connection = this.connections.get(row.machine.id);
|
|
315
|
+
if (connection) {
|
|
316
|
+
this.connections.delete(row.machine.id);
|
|
317
|
+
this.rejectPending(connection, "Runtime credential rotated; reconnect to reconcile");
|
|
318
|
+
connection.ws.close(1012, "Credential rotated");
|
|
319
|
+
}
|
|
320
|
+
this.emit({ type: "machine", machineId: row.machine.id });
|
|
321
|
+
}
|
|
322
|
+
return { machineId: row.machine.id, status: "rotated" };
|
|
323
|
+
}
|
|
324
|
+
handlePublicRequest(req, res, url) {
|
|
325
|
+
if (![RUNTIME_PAIR_PATH, RUNTIME_CLAIM_PATH, RUNTIME_ROTATE_PATH].includes(url.pathname))
|
|
326
|
+
return false;
|
|
327
|
+
const answer = (status, body) => { if (!res.destroyed && !res.writableEnded) {
|
|
328
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
329
|
+
res.end(JSON.stringify(body));
|
|
330
|
+
} };
|
|
331
|
+
if (this.closed || req.method !== "POST" || url.search || !privateTransport(req)) {
|
|
332
|
+
answer(403, { error: "Runtime pairing requires a protected non-browser connection" });
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
let charged = false;
|
|
336
|
+
void readBoundedWebBody(req, 4096).then(bytes => {
|
|
337
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
338
|
+
if (this.closed) {
|
|
339
|
+
answer(403, { error: "Runtime coordinator is closed" });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const key = this.requestRateKey(url.pathname, value, req);
|
|
343
|
+
charged = true;
|
|
344
|
+
if (!this.rate(key, key.startsWith("claim:") ? 120 : 30)) {
|
|
345
|
+
answer(429, { error: "Too many runtime pairing requests" });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (url.pathname === RUNTIME_ROTATE_PATH) {
|
|
349
|
+
if (!this.credentialOwner(req.headers.authorization)) {
|
|
350
|
+
answer(401, { error: "Runtime credential was refused" });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
answer(200, this.rotateCredential(req.headers.authorization, value));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (url.pathname === RUNTIME_PAIR_PATH)
|
|
357
|
+
answer(201, this.pair(value));
|
|
358
|
+
else {
|
|
359
|
+
const result = this.claim(value);
|
|
360
|
+
answer(result.status === "pending" ? 202 : 200, result);
|
|
361
|
+
}
|
|
362
|
+
}).catch(() => {
|
|
363
|
+
const allowed = charged || this.rate(`invalid:${req.socket.remoteAddress ?? "unknown"}`, 30);
|
|
364
|
+
answer(allowed ? 400 : 429, { error: "Runtime pairing request is invalid, expired or already used" });
|
|
365
|
+
});
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
handleUpgrade(req, socket, head) {
|
|
369
|
+
let url;
|
|
370
|
+
try {
|
|
371
|
+
url = new URL(req.url ?? "", "http://localhost");
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (url.pathname !== RUNTIME_CONNECT_PATH)
|
|
377
|
+
return false;
|
|
378
|
+
const refuse = () => { socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); };
|
|
379
|
+
const auth = req.headers.authorization;
|
|
380
|
+
if (this.closed || req.method !== "GET" || url.search || !privateTransport(req) || typeof auth !== "string" || !/^Bearer [a-f0-9]{64}$/i.test(auth)) {
|
|
381
|
+
refuse();
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
const row = this.credentialOwner(auth);
|
|
385
|
+
if (!row) {
|
|
386
|
+
refuse();
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
this.server.handleUpgrade(req, socket, head, ws => this.attach(ws, row));
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
current(connection) { return !this.closed && this.connections.get(connection.machineId) === connection && this.entry(connection.machineId).machine.status !== "revoked"; }
|
|
393
|
+
attach(ws, row) {
|
|
394
|
+
const old = this.connections.get(row.machine.id);
|
|
395
|
+
if (old) {
|
|
396
|
+
this.rejectPending(old, "Runtime connection was replaced; execution must be reconciled");
|
|
397
|
+
old.ws.close(1008, "Connection replaced");
|
|
398
|
+
}
|
|
399
|
+
row.machine.generation++;
|
|
400
|
+
row.machine.status = "offline";
|
|
401
|
+
this.save();
|
|
402
|
+
const connection = { ws, machineId: row.machine.id, generation: row.machine.generation, ready: false, alive: true, helloTimer: setTimeout(() => ws.close(1008, "Runtime hello required"), 10_000) };
|
|
403
|
+
connection.helloTimer.unref();
|
|
404
|
+
this.connections.set(row.machine.id, connection);
|
|
405
|
+
ws.on("pong", () => { connection.alive = true; if (this.current(connection))
|
|
406
|
+
row.machine.lastSeenAt = this.now(); });
|
|
407
|
+
ws.on("message", (data, binary) => { if (binary) {
|
|
408
|
+
ws.close(1008, "Runtime requires JSON text frames");
|
|
409
|
+
return;
|
|
410
|
+
} void this.message(connection, data).catch(() => ws.close(1008, "Invalid runtime message")); });
|
|
411
|
+
ws.on("error", () => { });
|
|
412
|
+
ws.on("close", () => {
|
|
413
|
+
clearTimeout(connection.helloTimer);
|
|
414
|
+
if (this.connections.get(row.machine.id) !== connection)
|
|
415
|
+
return;
|
|
416
|
+
this.connections.delete(row.machine.id);
|
|
417
|
+
this.rejectPending(connection, "Runtime disconnected; reconcile before retrying");
|
|
418
|
+
if (row.machine.status !== "revoked")
|
|
419
|
+
row.machine.status = "offline";
|
|
420
|
+
this.save();
|
|
421
|
+
this.emit({ type: "machine", machineId: row.machine.id });
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
send(connection, value) {
|
|
425
|
+
if (!this.current(connection) || connection.ws.readyState !== WebSocket.OPEN)
|
|
426
|
+
throw Error("Runtime machine is offline");
|
|
427
|
+
const frame = JSON.stringify(value);
|
|
428
|
+
if (Buffer.byteLength(frame) > RUNTIME_MAX_FRAME_BYTES)
|
|
429
|
+
throw Error("Runtime response exceeds the frame limit");
|
|
430
|
+
if (connection.ws.bufferedAmount > RUNTIME_MAX_FRAME_BYTES * 2) {
|
|
431
|
+
connection.ws.close(1013, "Runtime backpressure");
|
|
432
|
+
throw Error("Runtime connection is too slow");
|
|
433
|
+
}
|
|
434
|
+
connection.ws.send(frame);
|
|
435
|
+
}
|
|
436
|
+
rejectPending(connection, reason) {
|
|
437
|
+
for (const [id, pending] of this.panePending)
|
|
438
|
+
if (pending.machineId === connection.machineId && pending.generation === connection.generation) {
|
|
439
|
+
this.panePending.delete(id);
|
|
440
|
+
clearTimeout(pending.timer);
|
|
441
|
+
pending.reject(Error(reason));
|
|
442
|
+
}
|
|
443
|
+
let changed = false;
|
|
444
|
+
for (const [id, pending] of this.pending)
|
|
445
|
+
if (pending.machineId === connection.machineId && pending.generation === connection.generation) {
|
|
446
|
+
this.pending.delete(id);
|
|
447
|
+
clearTimeout(pending.timer);
|
|
448
|
+
const command = this.state.commands.find(row => row.id === id);
|
|
449
|
+
if (command?.state === "pending") {
|
|
450
|
+
command.state = interrupted(command);
|
|
451
|
+
command.error = reason;
|
|
452
|
+
changed = true;
|
|
453
|
+
}
|
|
454
|
+
pending.reject(Error(reason));
|
|
455
|
+
}
|
|
456
|
+
if (changed)
|
|
457
|
+
this.save();
|
|
458
|
+
}
|
|
459
|
+
validateTask(command, value) {
|
|
460
|
+
if (command.operation !== "run" || !record(command.payload) || !record(command.payload.task) || !record(value))
|
|
461
|
+
throw Error("Unexpected runtime task");
|
|
462
|
+
const input = command.payload.task;
|
|
463
|
+
if (value.id !== input.requestId || value.cli !== input.cli || value.cwd !== input.cwd || value.sandbox !== input.sandbox || (value.model ?? "") !== (input.model ?? "")
|
|
464
|
+
|| !["starting", "running", "cancelling", "completed", "failed", "cancelled"].includes(String(value.status))
|
|
465
|
+
|| typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt) || typeof value.output !== "string" || value.output.length > 64 * 1024
|
|
466
|
+
|| value.finishedAt !== undefined && (typeof value.finishedAt !== "number" || !Number.isFinite(value.finishedAt))
|
|
467
|
+
|| ["completed", "failed", "cancelled"].includes(String(value.status)) && (typeof value.finishedAt !== "number" || typeof value.outcomeConfirmed !== "boolean")
|
|
468
|
+
|| value.error !== undefined && (typeof value.error !== "string" || value.error.length > 8192)
|
|
469
|
+
|| value.threadId !== undefined && !text(value.threadId, 256)
|
|
470
|
+
|| value.threadId !== undefined && command.payload.resumeSessionId !== undefined && value.threadId !== command.payload.resumeSessionId
|
|
471
|
+
|| value.outcomeConfirmed !== undefined && typeof value.outcomeConfirmed !== "boolean")
|
|
472
|
+
throw Error("Invalid runtime task snapshot");
|
|
473
|
+
return { id: value.id, cli: value.cli, cwd: value.cwd, sandbox: value.sandbox, ...(value.model ? { model: value.model } : {}),
|
|
474
|
+
status: value.status, startedAt: value.startedAt, output: value.output,
|
|
475
|
+
...(value.finishedAt !== undefined ? { finishedAt: value.finishedAt } : {}), ...(value.error ? { error: value.error } : {}), ...(value.threadId ? { threadId: value.threadId } : {}), ...(value.outcomeConfirmed !== undefined ? { outcomeConfirmed: value.outcomeConfirmed } : {}) };
|
|
476
|
+
}
|
|
477
|
+
async message(connection, raw) {
|
|
478
|
+
if (!this.current(connection))
|
|
479
|
+
return;
|
|
480
|
+
const value = JSON.parse(raw.toString());
|
|
481
|
+
if (!record(value) || value.v !== 1 || !text(value.type, 32))
|
|
482
|
+
throw Error("Invalid runtime frame");
|
|
483
|
+
const row = this.entry(connection.machineId);
|
|
484
|
+
connection.alive = true;
|
|
485
|
+
if (!connection.ready) {
|
|
486
|
+
if (value.type !== "hello" || !text(value.bootId, 128))
|
|
487
|
+
throw Error("Runtime hello required");
|
|
488
|
+
connection.ready = true;
|
|
489
|
+
connection.bootId = value.bootId;
|
|
490
|
+
clearTimeout(connection.helloTimer);
|
|
491
|
+
row.machine.bootId = value.bootId;
|
|
492
|
+
row.machine.status = "online";
|
|
493
|
+
row.machine.lastSeenAt = this.now();
|
|
494
|
+
if (value.paneProtocol === 1)
|
|
495
|
+
row.machine.paneProtocol = 1;
|
|
496
|
+
else
|
|
497
|
+
delete row.machine.paneProtocol;
|
|
498
|
+
if (value.inventory !== undefined) {
|
|
499
|
+
row.machine.inventory = inventory(value.inventory);
|
|
500
|
+
row.machine.inventoryUpdatedAt = this.now();
|
|
501
|
+
}
|
|
502
|
+
this.save();
|
|
503
|
+
this.send(connection, { v: 1, type: "welcome", machineId: connection.machineId, generation: connection.generation });
|
|
504
|
+
this.emit({ type: "machine", machineId: connection.machineId });
|
|
505
|
+
// alpha.2 polish (dev-claude's no-chat e2e): a runner that brings no CLI inventory (just paired, nothing detected
|
|
506
|
+
// yet) is asked for its own report now — its detection runs on that PC, never on this fleet's loop — so the first
|
|
507
|
+
// approval of an agent's request there is not refused for "no available CLI" until someone presses Refresh.
|
|
508
|
+
// (#3004 rig: the runner's hello may carry a placeholder — `probedAt: null`, no CLIs — which is no report either.)
|
|
509
|
+
// (#3005 rig recheck: `force` — without it the runner answers with its cached inventory, on a fresh profile the same
|
|
510
|
+
// placeholder; Refresh sends force, and so does this: detection runs on that PC.)
|
|
511
|
+
if (!inventoryProbed(row.machine.inventory))
|
|
512
|
+
void this.call(connection.machineId, "probe", { force: true }).catch(() => { });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
row.machine.lastSeenAt = this.now();
|
|
516
|
+
if (value.type === "pane.tool.request") {
|
|
517
|
+
if (row.machine.paneProtocol !== 1 || Object.keys(value).some(key => !["v", "type", "id", "body"].includes(key)) || !UUID.test(String(value.id)))
|
|
518
|
+
throw Error("Invalid pane MCP transport request");
|
|
519
|
+
const body = validatePaneMcpBody(value.body), id = value.id;
|
|
520
|
+
const key = `${connection.machineId}:${connection.generation}:${id}`;
|
|
521
|
+
if (this.paneToolPending.size >= 128 || this.paneToolPending.has(key))
|
|
522
|
+
throw Error("Pane MCP transport backlog or duplicate request");
|
|
523
|
+
let active = true;
|
|
524
|
+
const current = () => active && !this.closed && this.current(connection) && connection.ready && connection.ws.readyState === WebSocket.OPEN;
|
|
525
|
+
this.paneToolPending.add(key);
|
|
526
|
+
let timer;
|
|
527
|
+
try {
|
|
528
|
+
if (!current() || !this.options.handlePaneMcp)
|
|
529
|
+
throw Error("Pane MCP scope unavailable");
|
|
530
|
+
const result = await Promise.race([this.options.handlePaneMcp(connection.machineId, body, current), new Promise((_resolve, reject) => {
|
|
531
|
+
timer = setTimeout(() => { active = false; reject(Error("Pane MCP deadline")); }, 80_000);
|
|
532
|
+
timer.unref();
|
|
533
|
+
})]);
|
|
534
|
+
if (current())
|
|
535
|
+
this.send(connection, { v: 1, type: "pane.tool.result", id, ok: true, result });
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
if (current())
|
|
539
|
+
this.send(connection, { v: 1, type: "pane.tool.result", id, ok: false });
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
active = false;
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
this.paneToolPending.delete(key);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (value.type === "pane.output") {
|
|
549
|
+
if (row.machine.paneProtocol !== 1)
|
|
550
|
+
throw Error("Pane protocol was not negotiated");
|
|
551
|
+
const snapshot = validateRuntimePaneSnapshot(value.snapshot);
|
|
552
|
+
this.emit({ type: "pane", machineId: connection.machineId, pane: snapshot });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (value.type === "pane.response") {
|
|
556
|
+
if (row.machine.paneProtocol !== 1 || !UUID.test(String(value.id)) || typeof value.ok !== "boolean")
|
|
557
|
+
throw Error("Invalid pane transport response");
|
|
558
|
+
const pending = this.panePending.get(value.id);
|
|
559
|
+
if (!pending)
|
|
560
|
+
return; // A late reply proves nothing and never replays a command.
|
|
561
|
+
if (pending.machineId !== connection.machineId || pending.generation !== connection.generation)
|
|
562
|
+
throw Error("Pane response belongs to another connection");
|
|
563
|
+
this.panePending.delete(value.id);
|
|
564
|
+
clearTimeout(pending.timer);
|
|
565
|
+
if (value.ok)
|
|
566
|
+
pending.resolve(value.result);
|
|
567
|
+
else
|
|
568
|
+
pending.reject(Error(typeof value.error === "string" ? value.error.slice(0, 1000) : "Pane operation failed"));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (value.type === "inventory") {
|
|
572
|
+
row.machine.inventory = inventory(value.inventory);
|
|
573
|
+
row.machine.inventoryUpdatedAt = this.now();
|
|
574
|
+
this.save();
|
|
575
|
+
this.emit({ type: "machine", machineId: connection.machineId });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (value.type === "task") {
|
|
579
|
+
if (!record(value.task) || !Number.isSafeInteger(value.seq) || value.seq < 1)
|
|
580
|
+
throw Error("Invalid runtime task event");
|
|
581
|
+
const taskValue = value.task;
|
|
582
|
+
const command = this.state.commands.find(command => command.id === taskValue.id && command.machineId === connection.machineId && command.operation === "run");
|
|
583
|
+
if (!command) {
|
|
584
|
+
// #2945 review: a reconnecting runtime republishes what it retained; a run already settled and compacted here
|
|
585
|
+
// is acknowledged by its tombstone and changes nothing. An unknown id, or another machine's, still closes.
|
|
586
|
+
if (this.tombstoned(connection.machineId, taskValue.id))
|
|
587
|
+
return;
|
|
588
|
+
throw Error("Runtime task does not belong to this machine");
|
|
589
|
+
}
|
|
590
|
+
const task = this.validateTask(command, value.task);
|
|
591
|
+
if (value.seq <= command.seq)
|
|
592
|
+
return;
|
|
593
|
+
if (command.task && terminal(command.task) && !terminal(task))
|
|
594
|
+
throw Error("Runtime task cannot restart");
|
|
595
|
+
command.seq = value.seq;
|
|
596
|
+
command.task = task;
|
|
597
|
+
command.state = "accepted";
|
|
598
|
+
this.save();
|
|
599
|
+
this.emit({ type: "task", machineId: connection.machineId, task });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (value.type === "response") {
|
|
603
|
+
if (!text(value.id, 128) || typeof value.ok !== "boolean")
|
|
604
|
+
throw Error("Invalid runtime response");
|
|
605
|
+
const pending = this.pending.get(value.id), command = this.state.commands.find(command => command.id === value.id);
|
|
606
|
+
if (!command || command.machineId !== connection.machineId) {
|
|
607
|
+
if (!command && this.tombstoned(connection.machineId, value.id))
|
|
608
|
+
return;
|
|
609
|
+
throw Error("Runtime response does not belong to this machine");
|
|
610
|
+
}
|
|
611
|
+
if (!pending || pending.generation !== connection.generation)
|
|
612
|
+
return;
|
|
613
|
+
this.pending.delete(value.id);
|
|
614
|
+
clearTimeout(pending.timer);
|
|
615
|
+
try {
|
|
616
|
+
if (!value.ok) {
|
|
617
|
+
command.state = "failed";
|
|
618
|
+
command.error = typeof value.error === "string" ? value.error.slice(0, 1024) : "Runtime request failed";
|
|
619
|
+
this.save();
|
|
620
|
+
pending.reject(Error(command.error));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
let result = value.result;
|
|
624
|
+
if (command.operation === "probe") {
|
|
625
|
+
result = inventory(result);
|
|
626
|
+
row.machine.inventory = result;
|
|
627
|
+
row.machine.inventoryUpdatedAt = this.now();
|
|
628
|
+
}
|
|
629
|
+
if (command.operation === "run") {
|
|
630
|
+
const task = this.validateTask(command, result);
|
|
631
|
+
if (!command.task || !terminal(command.task))
|
|
632
|
+
command.task = task;
|
|
633
|
+
result = command.task;
|
|
634
|
+
}
|
|
635
|
+
command.state = "accepted";
|
|
636
|
+
command.result = result;
|
|
637
|
+
this.save();
|
|
638
|
+
if (command.task)
|
|
639
|
+
this.emit({ type: "task", machineId: connection.machineId, task: command.task });
|
|
640
|
+
else if (command.operation === "probe")
|
|
641
|
+
this.emit({ type: "machine", machineId: connection.machineId });
|
|
642
|
+
pending.resolve(structuredClone(result));
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
command.state = "uncertain";
|
|
646
|
+
this.save();
|
|
647
|
+
pending.reject(error instanceof Error ? error : Error("Invalid runtime response"));
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (value.type === "tool.call") {
|
|
653
|
+
if (!text(value.id, 128) || !text(value.runId, 128))
|
|
654
|
+
throw Error("Invalid runtime tool call");
|
|
655
|
+
const command = this.state.commands.find(command => command.id === value.runId && command.machineId === connection.machineId && command.operation === "run");
|
|
656
|
+
try {
|
|
657
|
+
if (!command || !record(command.payload) || !record(command.payload.mcp) || command.task && terminal(command.task) || !this.options.handleMcp)
|
|
658
|
+
throw Error("Runtime tool call has no active turn");
|
|
659
|
+
const body = value.body;
|
|
660
|
+
if (!record(body) || Object.keys(body).some(key => !["op", "method", "params", "callId"].includes(key))
|
|
661
|
+
|| body.op !== "native-mcp" || !record(body.params) || Buffer.byteLength(JSON.stringify(body)) > 64 * 1024)
|
|
662
|
+
throw Error("Invalid runtime MCP request");
|
|
663
|
+
if (body.method === "tools/list") {
|
|
664
|
+
if (Object.keys(body.params).length || body.callId !== undefined)
|
|
665
|
+
throw Error("Invalid runtime MCP listing");
|
|
666
|
+
}
|
|
667
|
+
else if (body.method === "tools/call") {
|
|
668
|
+
if (!text(body.callId, 128) || !/^[A-Za-z0-9._:-]+$/.test(body.callId)
|
|
669
|
+
|| Object.keys(body.params).some(key => !["name", "arguments"].includes(key)) || typeof body.params.name !== "string"
|
|
670
|
+
|| !Array.isArray(command.payload.mcp.tools) || !command.payload.mcp.tools.includes(body.params.name)
|
|
671
|
+
|| body.params.arguments !== undefined && !record(body.params.arguments))
|
|
672
|
+
throw Error("Runtime MCP tool is outside this turn");
|
|
673
|
+
stable(body.params);
|
|
674
|
+
}
|
|
675
|
+
else
|
|
676
|
+
throw Error("Invalid runtime MCP operation");
|
|
677
|
+
const result = await this.options.handleMcp(connection.machineId, value.runId, body);
|
|
678
|
+
if (!this.current(connection))
|
|
679
|
+
return;
|
|
680
|
+
this.send(connection, { v: 1, type: "tool.result", id: value.id, ok: true, result });
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
if (this.current(connection))
|
|
684
|
+
this.send(connection, { v: 1, type: "tool.result", id: value.id, ok: false, error: "Runtime tool call was refused or its turn expired" });
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
throw Error("Unknown runtime message");
|
|
689
|
+
}
|
|
690
|
+
tombstoned(machineId, id) {
|
|
691
|
+
return typeof id === "string" && this.state.tombstones.some(row => row.id === id && row.machineId === machineId);
|
|
692
|
+
}
|
|
693
|
+
/** M-2239 P2-1: settled commands to tombstones (see COMMAND_KEEP_MS); never a pending, uncertain or unconfirmed one. */
|
|
694
|
+
compact() {
|
|
695
|
+
const now = this.now();
|
|
696
|
+
const settled = (command) => !this.pending.has(command.id) && (command.state === "failed"
|
|
697
|
+
|| command.state === "accepted" && (command.operation !== "run" || !!command.task && terminal(command.task) && command.task.outcomeConfirmed === true));
|
|
698
|
+
const drop = new Set(this.state.commands.filter(command => settled(command) && now - command.createdAt > COMMAND_KEEP_MS));
|
|
699
|
+
const excess = this.state.commands.length - drop.size - COMMAND_SOFT_CAP;
|
|
700
|
+
if (excess > 0)
|
|
701
|
+
for (const command of this.state.commands.filter(command => settled(command) && !drop.has(command)).sort((a, b) => a.createdAt - b.createdAt).slice(0, excess))
|
|
702
|
+
drop.add(command);
|
|
703
|
+
if (!drop.size)
|
|
704
|
+
return;
|
|
705
|
+
// Room for new tombstones without passing TOMBSTONE_CAP (what the constructor reads back, #2945 review): old ones
|
|
706
|
+
// past TOMBSTONE_KEEP_MS give way, oldest first; a younger one never does. Commands beyond that room stay as they
|
|
707
|
+
// are — still refused by id from the journal — until room frees up.
|
|
708
|
+
const tombstones = this.state.tombstones;
|
|
709
|
+
const evictable = tombstones.filter(row => now - row.at > TOMBSTONE_KEEP_MS).sort((a, b) => a.at - b.at);
|
|
710
|
+
const room = TOMBSTONE_CAP - tombstones.length + evictable.length;
|
|
711
|
+
const moved = [...drop].sort((a, b) => a.createdAt - b.createdAt).slice(0, Math.max(0, room));
|
|
712
|
+
if (!moved.length)
|
|
713
|
+
return;
|
|
714
|
+
const gone = new Set(evictable.slice(0, Math.max(0, tombstones.length + moved.length - TOMBSTONE_CAP)));
|
|
715
|
+
const movedSet = new Set(moved);
|
|
716
|
+
this.state.tombstones = [...tombstones.filter(row => !gone.has(row)),
|
|
717
|
+
...moved.map(command => ({ id: command.id, machineId: command.machineId, digest: command.digest, at: now }))];
|
|
718
|
+
this.state.commands = this.state.commands.filter(command => !movedSet.has(command));
|
|
719
|
+
this.save();
|
|
720
|
+
}
|
|
721
|
+
task(id) { const task = this.state.commands.find(command => command.id === id && command.operation === "run")?.task; return task ? structuredClone(task) : null; }
|
|
722
|
+
tasks() { return this.state.commands.flatMap(command => command.operation === "run" && command.task ? [{ machineId: command.machineId, task: structuredClone(command.task) }] : []); }
|
|
723
|
+
hasActiveTask(machineId) { return this.state.commands.some(command => command.machineId === machineId && command.operation === "run" && command.state !== "failed" && (!command.task || !terminal(command.task) || command.task.outcomeConfirmed === false)); }
|
|
724
|
+
paneTransportEpoch(machineId) {
|
|
725
|
+
const connection = this.connections.get(machineId);
|
|
726
|
+
return connection?.ready && connection.ws.readyState === WebSocket.OPEN && this.current(connection) ? `${this.paneEpoch}:${machineId}:${connection.generation}` : null;
|
|
727
|
+
}
|
|
728
|
+
/** Pane input replay/sequence authority lives in its pane ledger; reads never fill the exec-task journal. */
|
|
729
|
+
async callPane(machineId, raw, maxAgents = 4) {
|
|
730
|
+
const request = validateRuntimePaneRequest(raw);
|
|
731
|
+
if (!Number.isInteger(maxAgents) || maxAgents < 1 || maxAgents > 16)
|
|
732
|
+
throw Error("Invalid machine pane limit");
|
|
733
|
+
const machine = this.entry(machineId).machine, connection = this.connections.get(machineId);
|
|
734
|
+
if (this.closed || machine.status !== "online" || machine.paneProtocol !== 1 || !connection?.ready || !this.current(connection))
|
|
735
|
+
throw Error("Pane runtime is offline or unsupported");
|
|
736
|
+
if (this.panePending.size >= 64)
|
|
737
|
+
throw Error("Pane transport capacity reached");
|
|
738
|
+
const id = randomUUID();
|
|
739
|
+
return new Promise((resolve, reject) => {
|
|
740
|
+
const timer = setTimeout(() => { this.panePending.delete(id); reject(Error("Pane response timed out; reconcile before another input")); }, this.options.requestTimeoutMs ?? 30_000);
|
|
741
|
+
timer.unref();
|
|
742
|
+
this.panePending.set(id, { machineId, generation: connection.generation, resolve, reject, timer });
|
|
743
|
+
try {
|
|
744
|
+
this.send(connection, { v: 1, type: "pane.request", id, maxAgents, request });
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
this.panePending.delete(id);
|
|
748
|
+
clearTimeout(timer);
|
|
749
|
+
reject(error);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
/** Explicit switch-off only, after owned pane closes. A normal hub close/restart never shuts its hosts down. */
|
|
754
|
+
async shutdownPaneHosts() {
|
|
755
|
+
const machines = this.listMachines().filter(machine => machine.status === "online" && machine.paneProtocol === 1);
|
|
756
|
+
const outcomes = await Promise.allSettled(machines.map(async (machine) => {
|
|
757
|
+
const raw = await this.callPane(machine.id, { action: "list" });
|
|
758
|
+
if (!raw || typeof raw.hostBootId !== "string" || !/^[a-f0-9-]{36}$/i.test(raw.hostBootId) || !Array.isArray(raw.panes) || raw.panes.length > 4096)
|
|
759
|
+
throw Error("Pane shutdown inventory is unconfirmed");
|
|
760
|
+
if (raw.panes.map(validateRuntimePaneSnapshot).some(pane => pane.state !== "exited"))
|
|
761
|
+
throw Error("Pane shutdown still has live or unconfirmed work");
|
|
762
|
+
validateRuntimePaneShutdown(await this.callPane(machine.id, { action: "shutdown", hostBootId: raw.hostBootId }), raw.hostBootId);
|
|
763
|
+
}));
|
|
764
|
+
if (outcomes.some(result => result.status === "rejected"))
|
|
765
|
+
throw Error("One or more pane hosts could not confirm empty shutdown; retained work stays reserved");
|
|
766
|
+
}
|
|
767
|
+
async call(machineId, operation, payload) {
|
|
768
|
+
if (this.closed || !["probe", "workspace", "run", "cancel", "reconcile", "recover"].includes(operation))
|
|
769
|
+
throw Error("Unsupported runtime operation");
|
|
770
|
+
const machine = this.entry(machineId).machine;
|
|
771
|
+
const connection = this.connections.get(machineId);
|
|
772
|
+
if (machine.status !== "online" || !connection?.ready || !this.current(connection))
|
|
773
|
+
throw Error("Runtime machine is offline");
|
|
774
|
+
let id = randomUUID(), normalized = payload;
|
|
775
|
+
if (operation === "run") {
|
|
776
|
+
if (!record(payload) || Object.keys(payload).some(k => !["task", "resumeSessionId", "context", "mcp"].includes(k)))
|
|
777
|
+
throw Error("Invalid runtime run payload");
|
|
778
|
+
const task = validateNativeTask(payload.task);
|
|
779
|
+
if (payload.resumeSessionId !== undefined && !text(payload.resumeSessionId, 256) || payload.context !== undefined && (typeof payload.context !== "string" || payload.context.length > 2000 || payload.context.includes("\0")))
|
|
780
|
+
throw Error("Invalid runtime turn context");
|
|
781
|
+
if (payload.mcp !== undefined && (!record(payload.mcp) || Object.keys(payload.mcp).some(k => k !== "tools") || !Array.isArray(payload.mcp.tools) || !payload.mcp.tools.length || payload.mcp.tools.length > 128 || payload.mcp.tools.some(v => typeof v !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(v))))
|
|
782
|
+
throw Error("Invalid runtime tool scope");
|
|
783
|
+
normalized = { ...payload, task };
|
|
784
|
+
id = task.requestId;
|
|
785
|
+
}
|
|
786
|
+
if (operation === "recover") {
|
|
787
|
+
if (!record(payload) || Object.keys(payload).some(key => !["runId", "confirmedStopped"].includes(key))
|
|
788
|
+
|| !text(payload.runId, 80) || payload.confirmedStopped !== true)
|
|
789
|
+
throw Error("Runtime recovery requires explicit stopped-process confirmation");
|
|
790
|
+
const original = this.state.commands.find(command => command.id === payload.runId && command.machineId === machineId && command.operation === "run");
|
|
791
|
+
if (!original)
|
|
792
|
+
throw Error("Runtime recovery does not belong to this machine");
|
|
793
|
+
// A runtime that never received the original request can still save a no-execution tombstone after manual
|
|
794
|
+
// confirmation. The original is server-owned; callers cannot replace it with another task or tool scope.
|
|
795
|
+
normalized = { runId: payload.runId, confirmedStopped: true, original: structuredClone(original.payload) };
|
|
796
|
+
}
|
|
797
|
+
const digest = hash(stable({ operation, payload: normalized }));
|
|
798
|
+
if (Buffer.byteLength(JSON.stringify(normalized)) > RUNTIME_MAX_FRAME_BYTES / 2)
|
|
799
|
+
throw Error("Runtime command payload is too large");
|
|
800
|
+
const existing = this.state.commands.find(command => command.id === id);
|
|
801
|
+
if (existing) {
|
|
802
|
+
if (existing.machineId !== machineId || existing.digest !== digest)
|
|
803
|
+
throw Error("Runtime task ID belongs to a different request or machine");
|
|
804
|
+
if (existing.task)
|
|
805
|
+
return structuredClone(existing.task);
|
|
806
|
+
throw Error("Runtime command is already recorded; reconcile it before retrying");
|
|
807
|
+
}
|
|
808
|
+
const tombstone = this.state.tombstones.find(row => row.id === id);
|
|
809
|
+
if (tombstone) {
|
|
810
|
+
if (tombstone.machineId !== machineId || tombstone.digest !== digest)
|
|
811
|
+
throw Error("Runtime task ID belongs to a different request or machine");
|
|
812
|
+
throw Error("Runtime command already ran and its record was compacted; do not retry it");
|
|
813
|
+
}
|
|
814
|
+
if (operation === "run" && this.hasActiveTask(machineId))
|
|
815
|
+
throw Error("Runtime machine is busy or has an unconfirmed task");
|
|
816
|
+
this.compact();
|
|
817
|
+
if (this.state.commands.length >= COMMAND_HARD_CAP)
|
|
818
|
+
throw Error("Runtime command journal limit reached: too many unsettled commands; reconcile them");
|
|
819
|
+
const command = { id, machineId, operation, digest, payload: normalized, createdAt: this.now(), state: "pending", seq: 0 };
|
|
820
|
+
// Journal is committed before the first byte reaches the execution computer. Never auto-send it again.
|
|
821
|
+
this.state.commands.push(command);
|
|
822
|
+
this.save();
|
|
823
|
+
return new Promise((resolve, reject) => {
|
|
824
|
+
const timer = setTimeout(() => {
|
|
825
|
+
this.pending.delete(id);
|
|
826
|
+
command.state = interrupted(command);
|
|
827
|
+
command.error = command.state === "failed" ? "Runtime inventory request timed out" : "Runtime acknowledgment timed out; reconcile before retrying";
|
|
828
|
+
this.save();
|
|
829
|
+
reject(Error(command.error));
|
|
830
|
+
}, this.options.requestTimeoutMs ?? (operation === "probe" ? 90_000 : 30_000));
|
|
831
|
+
timer.unref();
|
|
832
|
+
this.pending.set(id, { machineId, generation: connection.generation, resolve, reject, timer });
|
|
833
|
+
try {
|
|
834
|
+
this.send(connection, { v: 1, type: "request", id, operation, payload: normalized });
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
this.pending.delete(id);
|
|
838
|
+
clearTimeout(timer);
|
|
839
|
+
command.state = interrupted(command);
|
|
840
|
+
this.save();
|
|
841
|
+
reject(error);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
close() {
|
|
846
|
+
if (this.closed)
|
|
847
|
+
return;
|
|
848
|
+
this.closed = true;
|
|
849
|
+
clearInterval(this.heartbeat);
|
|
850
|
+
for (const connection of this.connections.values()) {
|
|
851
|
+
clearTimeout(connection.helloTimer);
|
|
852
|
+
this.rejectPending(connection, "Runtime coordinator stopped; reconcile before retrying");
|
|
853
|
+
connection.ws.terminate();
|
|
854
|
+
}
|
|
855
|
+
this.connections.clear();
|
|
856
|
+
for (const row of this.state.machines)
|
|
857
|
+
if (row.machine.status === "online")
|
|
858
|
+
row.machine.status = "offline";
|
|
859
|
+
this.save();
|
|
860
|
+
this.server.close();
|
|
861
|
+
this.listeners.clear();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
//# sourceMappingURL=runtime-coordinator.js.map
|