@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
package/dist/fleet-manager.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { buildNativeFleetInstructions } from "./instructions.js";
|
|
2
|
+
import { GENERAL_ROLE_INSTRUCTIONS } from "./general-role.js";
|
|
3
|
+
import { NativePaneRecoveryError } from "./native-pane-recovery.js";
|
|
4
|
+
import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
5
|
+
import { windowsAgentsFor } from "./windows-agents.js";
|
|
1
6
|
import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
2
7
|
import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
|
|
3
8
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
@@ -9,10 +14,16 @@ import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecovery
|
|
|
9
14
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
10
15
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
11
16
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
12
|
-
import {
|
|
17
|
+
import { prepareSettingsEffect } from "./settings-effect.js";
|
|
18
|
+
import { settingsDisplay } from "./settings-change.js";
|
|
19
|
+
import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
13
20
|
import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
|
|
14
21
|
import { SettingsControlServer } from "./settings-control.js";
|
|
15
|
-
import {
|
|
22
|
+
import { SettingsRouteLifetime } from "./settings-route-lifetime.js";
|
|
23
|
+
import { GeneralManagedFileError } from "./runtime-general-protocol.js";
|
|
24
|
+
import { GeneralBindings, generalBindingRevision, parseGeneralSwitchInput, switchGeneralLocation } from "./general-binding.js";
|
|
25
|
+
import { confirmGeneralHostExit } from "./general-host-owner.js";
|
|
26
|
+
import { settingsFingerprint, SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
16
27
|
import { performance } from "node:perf_hooks";
|
|
17
28
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
18
29
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
@@ -42,7 +53,7 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
42
53
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
|
|
43
54
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
44
55
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
45
|
-
import { freemem, totalmem, homedir } from "node:os";
|
|
56
|
+
import { freemem, totalmem, homedir, hostname } from "node:os";
|
|
46
57
|
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
47
58
|
import { createServer } from "node:http";
|
|
48
59
|
import { Worker } from "node:worker_threads";
|
|
@@ -84,7 +95,8 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
|
84
95
|
import { TmuxManager } from "./tmux-manager.js";
|
|
85
96
|
import { AccessManager } from "./channel/access-manager.js";
|
|
86
97
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
87
|
-
import {
|
|
98
|
+
import { NativeDaemonClient } from "./native-daemon-client.js";
|
|
99
|
+
import { usesChannelAddress, TOOLS } from "./channel/mcp-tools.js";
|
|
88
100
|
import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
|
|
89
101
|
import { formatFleetTime } from "./tz-utils.js";
|
|
90
102
|
import { createAdapter } from "./channel/factory.js";
|
|
@@ -94,6 +106,7 @@ import { createBackend } from "./backend/factory.js";
|
|
|
94
106
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
95
107
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
96
108
|
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
109
|
+
import { readGrokModelCache } from "./backend/grok-model-cache.js";
|
|
97
110
|
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
98
111
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
99
112
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
@@ -168,6 +181,16 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
168
181
|
return tasks;
|
|
169
182
|
}
|
|
170
183
|
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
184
|
+
import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
|
|
185
|
+
import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
|
|
186
|
+
import { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
187
|
+
import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
|
|
188
|
+
import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
|
|
189
|
+
import { NativeMcpSessions } from "./native-mcp.js";
|
|
190
|
+
import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
|
|
191
|
+
import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
|
|
192
|
+
import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
|
|
193
|
+
import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
|
|
171
194
|
import { LogService } from "./log-service.js";
|
|
172
195
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
173
196
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
@@ -269,6 +292,7 @@ import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninP
|
|
|
269
292
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
270
293
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
271
294
|
import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
|
|
295
|
+
import { isOutsideLinkOwner, OUTSIDE_LINK_OWNER, OUTSIDE_LINK_RETRY_MS, OUTSIDE_OWN_CLOSES, outsideDuration, outsideTimer, TOTP_STEP_SECONDS, WEB_REMOTE_ACCESS_FILE, WebRemoteAccess } from "./web-remote-access.js";
|
|
272
296
|
import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
273
297
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
274
298
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
@@ -737,6 +761,825 @@ export class FleetManager {
|
|
|
737
761
|
/** Live view of lifecycle.daemons — used throughout; not deprecated. */
|
|
738
762
|
get daemons() { return this.lifecycle.daemons; }
|
|
739
763
|
fleetConfig = null;
|
|
764
|
+
fleetWindowsRuntime = null;
|
|
765
|
+
nativeMcpStore = null;
|
|
766
|
+
paneManager = null;
|
|
767
|
+
localPaneRelay = null;
|
|
768
|
+
openingLocalPane = null;
|
|
769
|
+
runtimeHub = null;
|
|
770
|
+
runtimeExecutor = null;
|
|
771
|
+
runtimeStatusScheduled = false;
|
|
772
|
+
reconciledRemoteResults = new Set();
|
|
773
|
+
/** Machine credentials are separate from dashboard sessions and per-turn agent capabilities. */
|
|
774
|
+
runtimeCoordinator() {
|
|
775
|
+
if (!remoteRuntimeEnabled())
|
|
776
|
+
throw new Error("Remote runtimes are not enabled");
|
|
777
|
+
return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
|
|
778
|
+
handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
|
|
779
|
+
handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
|
|
780
|
+
changed: event => {
|
|
781
|
+
if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
|
|
782
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
|
|
783
|
+
if (config.machine_id === event.machineId) {
|
|
784
|
+
this.nativeMcpStore?.closeInstance(name);
|
|
785
|
+
if (config.general_topic) {
|
|
786
|
+
this.cancelPendingDeliveries(name);
|
|
787
|
+
this.holdGeneralOldDeliveries(name);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
this.needsYou?.poke();
|
|
791
|
+
}
|
|
792
|
+
if (this.runtimeStatusScheduled || this.shuttingDown)
|
|
793
|
+
return;
|
|
794
|
+
this.runtimeStatusScheduled = true;
|
|
795
|
+
queueMicrotask(() => {
|
|
796
|
+
this.runtimeStatusScheduled = false;
|
|
797
|
+
if (this.shuttingDown)
|
|
798
|
+
return;
|
|
799
|
+
try {
|
|
800
|
+
this.fleetWindowsRuntime?.importHistory();
|
|
801
|
+
for (const snapshot of this.runtimeHub?.tasks() ?? [])
|
|
802
|
+
this.reconcileRemoteDelivery(snapshot.machineId, snapshot.task);
|
|
803
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
804
|
+
this.scheduleDeliveryOutboxPump();
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
this.logger.warn({ err: error }, "Runtime status reconciliation failed");
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
runtimeWindowsExecutor() {
|
|
814
|
+
return this.runtimeExecutor ??= new RuntimeWindowsBridge(this, () => remoteRuntimeEnabled() && !this.shuttingDown ? this.runtimeCoordinator() : null);
|
|
815
|
+
}
|
|
816
|
+
/** Per-turn capabilities for native MCP, separate from browser and Linux-agent credentials. */
|
|
817
|
+
nativeMcpSessions() {
|
|
818
|
+
return this.nativeMcpStore ??= new NativeMcpSessions(this);
|
|
819
|
+
}
|
|
820
|
+
/** The same instance registry and delivery entry point, with a native Windows execution adapter. */
|
|
821
|
+
nativeInstanceRuntime() {
|
|
822
|
+
return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
|
|
823
|
+
owner: this, fleet: () => this.fleetConfig, save: () => this.saveFleetConfig(), stopping: () => this.shuttingDown, history: this.webChatHistory,
|
|
824
|
+
openMcp: (instance, requestId, sandbox, stillCurrent) => this.healthServerListening
|
|
825
|
+
? this.nativeMcpSessions().grant({ instance, requestId, sandbox, stillCurrent, endpoint: `http://127.0.0.1:${this.fleetConfig?.health_port ?? 19280}/agent` }) : null,
|
|
826
|
+
warn: (message, error) => this.logger.warn({ err: error }, message),
|
|
827
|
+
removed: name => this.nativeInstanceRemoved(name),
|
|
828
|
+
paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
|
|
829
|
+
machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
|
|
830
|
+
validateMachine: (id, cli, sandbox) => {
|
|
831
|
+
const machine = this.runtimeCoordinator().machine(id);
|
|
832
|
+
const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
|
|
833
|
+
if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
|
|
834
|
+
throw new Error("Choose an online runtime and an available CLI");
|
|
835
|
+
if (sandbox === "read-only" && !provider.readOnlySupport)
|
|
836
|
+
throw new Error("This runtime CLI cannot enforce read-only execution");
|
|
837
|
+
},
|
|
838
|
+
emit: (event, data) => {
|
|
839
|
+
this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
|
|
840
|
+
if (event === "activity")
|
|
841
|
+
this.scheduleDeliveryOutboxPump();
|
|
842
|
+
},
|
|
843
|
+
}, { bridge: this.runtimeWindowsExecutor() });
|
|
844
|
+
}
|
|
845
|
+
/** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
|
|
846
|
+
async createNativePaneInstance(profile, channelId, authorized, execution) {
|
|
847
|
+
const { createNativePane } = await import("./native-pane-create.js");
|
|
848
|
+
return createNativePane({ fleet: () => this.fleetConfig, runtime: this.nativeInstanceRuntime(),
|
|
849
|
+
topic: target => {
|
|
850
|
+
const world = this.worlds.get(target.id), adapter = world?.adapter;
|
|
851
|
+
if (!world || !adapter?.createTopic || !adapter.deleteTopic)
|
|
852
|
+
throw Error("The approved connection cannot create and compensate a topic");
|
|
853
|
+
const same = () => {
|
|
854
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
855
|
+
const matches = channels.filter(row => (row.id ?? row.type) === target.id);
|
|
856
|
+
return !this.shuttingDown && this.worlds.get(target.id) === world && world.adapter === adapter && matches.length === 1 && matches[0].type === target.type && matches[0].group_id === target.group_id;
|
|
857
|
+
};
|
|
858
|
+
return { current: same, create: name => adapter.createTopic(name), remove: id => adapter.deleteTopic(id) };
|
|
859
|
+
},
|
|
860
|
+
epoch: name => this.lifecycle.epochOf(name),
|
|
861
|
+
start: async (name, config, current) => {
|
|
862
|
+
if (!current())
|
|
863
|
+
throw Error("Native creation expired before start");
|
|
864
|
+
await this.startInstance(name, config, true);
|
|
865
|
+
if (!current())
|
|
866
|
+
throw Error("Native creation expired during start");
|
|
867
|
+
await this.connectIpcToInstance(name);
|
|
868
|
+
if (!current())
|
|
869
|
+
throw Error("Native creation expired during channel connection");
|
|
870
|
+
},
|
|
871
|
+
close: (name, config, epoch) => this.lifecycle.runTransition(name, async (transition) => {
|
|
872
|
+
if (this.fleetConfig?.instances[name] !== config || this.lifecycle.epochOf(name) !== epoch)
|
|
873
|
+
return false;
|
|
874
|
+
await this.stopInstance(name, transition);
|
|
875
|
+
return this.fleetConfig?.instances[name] === config && this.lifecycle.epochOf(name) === epoch + 1;
|
|
876
|
+
}),
|
|
877
|
+
routes: () => { if (this.fleetConfig)
|
|
878
|
+
this.routing.rebuild(this.fleetConfig); },
|
|
879
|
+
warn: error => this.logger.warn({ err: error }, "Native creation cleanup is held; retained identity and topic require attention"), }, profile, channelId, () => !this.shuttingDown && authorized(), execution);
|
|
880
|
+
}
|
|
881
|
+
/** Admin-only settings path: validate first, then close the exact old launch before changing cold fields. */
|
|
882
|
+
async updateNativePaneConfig(name, patch, authorized, transaction) {
|
|
883
|
+
return this.lifecycle.runTransition(name, async (transition) => {
|
|
884
|
+
const before = this.fleetConfig?.instances[name];
|
|
885
|
+
if (!isNativePaneInstance(before))
|
|
886
|
+
throw Error("Persistent Windows instance not found");
|
|
887
|
+
const runtime = this.nativeInstanceRuntime(), generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
888
|
+
let expectedGeneration = generation;
|
|
889
|
+
const current = () => authorized() && !this.shuttingDown && this.fleetConfig?.instances[name] === before
|
|
890
|
+
&& (this.explicitStopGeneration.get(name) ?? 0) === expectedGeneration
|
|
891
|
+
&& (before.machine_id ? remoteRuntimeEnabled() : localWindowsCliEnabled());
|
|
892
|
+
if (!current())
|
|
893
|
+
throw Error("Native settings authority expired");
|
|
894
|
+
runtime.previewUpdate(name, patch);
|
|
895
|
+
const cold = ["working_directory", "model", "systemPrompt", "native_sandbox"].some(key => Object.hasOwn(patch, key) && (patch[key] ?? "") !== (before[key] ?? ""));
|
|
896
|
+
const running = this.daemons.get(name);
|
|
897
|
+
if (cold) {
|
|
898
|
+
if (running && !running.nativePaneHasExited() && (!await running.nativePauseReady({ current, accepts: () => true, revalidate: async () => current() }) || !running.nativePauseStillReady()))
|
|
899
|
+
throw Error("Wait for this Windows agent to be idle before changing its launch settings");
|
|
900
|
+
if (!current())
|
|
901
|
+
throw Error("Native settings authority expired before closing its old pane");
|
|
902
|
+
await this.stopInstance(name, transition);
|
|
903
|
+
expectedGeneration = generation + 1;
|
|
904
|
+
if (!current())
|
|
905
|
+
throw Error("Native settings authority expired after closing its old pane");
|
|
906
|
+
if (runtime.get(name)?.activeId)
|
|
907
|
+
throw Error("Native pane exit is unconfirmed; its settings were not changed");
|
|
908
|
+
}
|
|
909
|
+
const result = runtime.update(name, patch, current, transaction), next = this.fleetConfig.instances[name];
|
|
910
|
+
if (cold && running) {
|
|
911
|
+
if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== next || (this.explicitStopGeneration.get(name) ?? 0) !== expectedGeneration)
|
|
912
|
+
throw Error("Native settings saved; restart authority expired");
|
|
913
|
+
await this.startInstance(name, next, true, "fleet-topic", false, transition);
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async localNativePaneEndpoint() {
|
|
919
|
+
const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
|
|
920
|
+
throw Error("Local Windows runtime is switched off or draining"); };
|
|
921
|
+
admit();
|
|
922
|
+
if (this.localPaneDrain)
|
|
923
|
+
throw Error("Local Windows runtime cleanup is pending");
|
|
924
|
+
if (this.localPaneRelay?.online())
|
|
925
|
+
return this.localPaneRelay;
|
|
926
|
+
if (this.openingLocalPane)
|
|
927
|
+
return this.openingLocalPane;
|
|
928
|
+
const opening = (async () => {
|
|
929
|
+
const [{ LocalPaneEndpoint }, { launchLocalPaneRelay, localPaneScope }, inventory] = await Promise.all([
|
|
930
|
+
import("./local-pane-endpoint.js"), import("./local-pane-launcher.js"), this.runtimeWindowsExecutor().probe({ force: false }),
|
|
931
|
+
]);
|
|
932
|
+
admit();
|
|
933
|
+
this.localPaneRelay?.stop();
|
|
934
|
+
const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
|
|
935
|
+
diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
|
|
936
|
+
version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
|
|
937
|
+
launch: () => launchLocalPaneRelay(this.dataDir, admit),
|
|
938
|
+
mcp: async (body, current) => (await this.nativePanes()).handle("local", body, current),
|
|
939
|
+
});
|
|
940
|
+
try {
|
|
941
|
+
admit();
|
|
942
|
+
this.localPaneRelay = relay;
|
|
943
|
+
return relay;
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
relay.stop();
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
})();
|
|
950
|
+
this.openingLocalPane = opening;
|
|
951
|
+
try {
|
|
952
|
+
return await opening;
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
if (this.openingLocalPane === opening)
|
|
956
|
+
this.openingLocalPane = null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/** Persistent ownership is independent of the exec worker's per-turn capabilities. */
|
|
960
|
+
async nativePanes() {
|
|
961
|
+
if (this.paneManager)
|
|
962
|
+
return this.paneManager;
|
|
963
|
+
const { NativePaneManager } = await import("./native-pane-manager.js");
|
|
964
|
+
if (this.paneManager)
|
|
965
|
+
return this.paneManager;
|
|
966
|
+
const store = windowsAgentsFor(this, this.runtimeWindowsExecutor());
|
|
967
|
+
const allowed = (name, tool) => nativePaneToolAvailable(tool) && mayUseTool(resolveToolSet(this.fleetConfig?.instances[name], name), tool)
|
|
968
|
+
&& !dashboardRefusalIn(this.fleetConfig, name, tool);
|
|
969
|
+
return this.paneManager = new NativePaneManager({
|
|
970
|
+
home: this.dataDir, store, config: name => this.fleetConfig?.instances[name],
|
|
971
|
+
freshNotice: async (name, provider) => {
|
|
972
|
+
const message = t(provider === "codex" ? "native_pane.codex_fresh_started" : "native_pane.fresh_started");
|
|
973
|
+
this.eventLog?.insert(name, "native_pane_fresh_started", {});
|
|
974
|
+
this.logger.info({ name }, message);
|
|
975
|
+
await this.notifyInstanceTopic(name, message);
|
|
976
|
+
},
|
|
977
|
+
admit: (name, config) => {
|
|
978
|
+
const now = this.fleetConfig?.instances[name];
|
|
979
|
+
if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
|
|
980
|
+
|| now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
|
|
981
|
+
|| now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
|
|
982
|
+
|| now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
|
|
983
|
+
throw Error("Native pane configuration or lifetime changed");
|
|
984
|
+
if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
|
|
985
|
+
throw Error("Native runtime is switched off");
|
|
986
|
+
},
|
|
987
|
+
endpoint: async (agent, cleanup = false) => {
|
|
988
|
+
if (!agent.machineId) {
|
|
989
|
+
if (!cleanup)
|
|
990
|
+
return this.localNativePaneEndpoint();
|
|
991
|
+
if (!this.localPaneRelay)
|
|
992
|
+
throw Error("Local cleanup transport is unavailable; pane remains reserved");
|
|
993
|
+
return this.localPaneRelay;
|
|
994
|
+
}
|
|
995
|
+
const hub = cleanup ? this.runtimeHub : this.runtimeCoordinator(), machineId = agent.machineId;
|
|
996
|
+
if (!hub)
|
|
997
|
+
throw Error("Remote cleanup transport is unavailable; pane remains reserved");
|
|
998
|
+
const draining = () => !!this.remotePaneDrain;
|
|
999
|
+
return { machineId, online: () => !this.shuttingDown && (draining() || remoteRuntimeEnabled()) && this.runtimeHub === hub && hub.machine(machineId)?.status === "online",
|
|
1000
|
+
call: request => {
|
|
1001
|
+
if (this.runtimeHub !== hub || !(draining() || remoteRuntimeEnabled()) || draining() && request.action !== "snapshot" && request.action !== "close")
|
|
1002
|
+
return Promise.reject(Error("Remote pane transport only permits owned cleanup"));
|
|
1003
|
+
return hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1004
|
+
}, subscribe: listener => hub.subscribe(listener),
|
|
1005
|
+
epoch: () => hub.paneTransportEpoch(machineId), version: provider => hub.machine(machineId)?.inventory?.clis.find(row => row.id === provider && row.runnable)?.version ?? null };
|
|
1006
|
+
},
|
|
1007
|
+
instructions: (name, config) => buildNativeFleetInstructions({ instanceName: name, workingDirectory: config.working_directory,
|
|
1008
|
+
displayName: config.display_name, description: config.description, customPrompt: config.systemPrompt,
|
|
1009
|
+
workflow: config.workflow, taskBoard: !dashboardRefusalIn(this.fleetConfig, name, "task"),
|
|
1010
|
+
runtimeIdentity: { kind: "fleet-topic", backend: config.backend ?? "codex", model: config.model || "default" } }, config.general_topic === true),
|
|
1011
|
+
menu: name => TOOLS.filter(tool => allowed(name, tool.name)).map(tool => ({ ...tool })),
|
|
1012
|
+
allowed: (name, tool, args) => allowed(name, tool) && !nativePaneToolRefusal(tool, args), published: (name, daemon) => this.daemons.get(name) === daemon,
|
|
1013
|
+
invoke: async (daemon, input, current, name) => {
|
|
1014
|
+
if (input.tool === "list_models") {
|
|
1015
|
+
const { ListModelsArgs } = await import("./outbound-schemas.js");
|
|
1016
|
+
const parsed = ListModelsArgs.safeParse(input.args);
|
|
1017
|
+
if (!parsed.success || !current())
|
|
1018
|
+
return { result: null, error: "Invalid native model request or expired owner" };
|
|
1019
|
+
const target = parsed.data.instance_name ?? name;
|
|
1020
|
+
if (!isWindowsInstance(this.fleetConfig?.instances[target]))
|
|
1021
|
+
return { result: null, error: "Native panes can only read Windows runtime model inventories" };
|
|
1022
|
+
return { result: await this.listModelCatalog({ instanceName: target }) };
|
|
1023
|
+
}
|
|
1024
|
+
const ops = { list_instances: "list", describe_instance: "describe", task: "task", send_to_instance: "send",
|
|
1025
|
+
delegate_task: "delegate", request_information: "ask", report_result: "report", delivery_status: "delivery-status" };
|
|
1026
|
+
const { nativeAgentAdmissionContext } = await import("./native-agent-admission.js");
|
|
1027
|
+
const { dispatchAgentOperation } = await import("./agent-endpoint.js");
|
|
1028
|
+
return daemon.handlePaneToolCall(input, { current, allowed: tool => allowed(name, tool),
|
|
1029
|
+
dispatch: (tool, args) => ops[tool] ? dispatchAgentOperation(nativeAgentAdmissionContext(this, {
|
|
1030
|
+
instance: name, current: () => current() && allowed(name, tool), taskAllowed: () => current() && allowed(name, "task"),
|
|
1031
|
+
}), name, ops[tool], args, input.operationId) : undefined });
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/** Prepare only bundled role files; this neither switches General nor starts a provider process. */
|
|
1036
|
+
async prepareWindowsGeneralWorkspace(name, machineId, cli, bindingId, authorized) {
|
|
1037
|
+
const config = this.fleetConfig?.instances[name], hub = this.runtimeCoordinator(), epoch = hub.paneTransportEpoch(machineId);
|
|
1038
|
+
if (!config?.general_topic || !epoch)
|
|
1039
|
+
throw Error("General or its paired runtime is unavailable");
|
|
1040
|
+
const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
|
|
1041
|
+
&& this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
|
|
1042
|
+
const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
|
|
1043
|
+
import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
|
|
1044
|
+
]);
|
|
1045
|
+
if (!current())
|
|
1046
|
+
throw Error("General workspace authority expired");
|
|
1047
|
+
assertAuditedNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
|
|
1048
|
+
const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
|
|
1049
|
+
const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
|
|
1050
|
+
if (!current())
|
|
1051
|
+
throw Error("General workspace authority expired");
|
|
1052
|
+
if (inventory?.capabilities?.generalWorkspace !== true)
|
|
1053
|
+
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1054
|
+
const result = await hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1055
|
+
if (!current())
|
|
1056
|
+
throw Error("General workspace authority expired");
|
|
1057
|
+
if (generalWorkspaceUpgrade(result, request))
|
|
1058
|
+
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1059
|
+
return validateGeneralWorkspaceReceipt(result, request);
|
|
1060
|
+
}
|
|
1061
|
+
generalLocationFlights = new Map();
|
|
1062
|
+
generalAttentionEpisodes = new Map();
|
|
1063
|
+
generalBindings() { return new GeneralBindings(this.dataDir); }
|
|
1064
|
+
generalLocations() {
|
|
1065
|
+
const store = this.generalBindings();
|
|
1066
|
+
return { generals: Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => cfg.general_topic).map(([name, cfg]) => {
|
|
1067
|
+
const record = store.read(name), machine = cfg.machine_id ? this.runtimeHub?.machine(cfg.machine_id) : null;
|
|
1068
|
+
return { name, displayName: cfg.display_name ?? name, location: isWindowsInstance(cfg) ? "windows" : "host",
|
|
1069
|
+
machineId: cfg.machine_id ?? null, cli: cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code", model: cfg.model ?? "",
|
|
1070
|
+
revision: generalBindingRevision(record, cfg), canSwitchBack: !!record && record.active !== "host",
|
|
1071
|
+
offline: isWindowsInstance(cfg) && (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(cfg.machine_id)),
|
|
1072
|
+
retryTarget: record?.pending?.phase === "selected" ? (record.pending.to === "host" ? { location: "host" } : (() => { const row = record.windows.find(item => item.id === record.pending.to); return { location: "windows", machineId: row.machineId, cli: row.cli, model: row.config.model ?? "" }; })()) : null,
|
|
1073
|
+
hold: record?.hold ?? (record?.pending ? record.pending.phase === "selected" ? "start" : "exit" : null), switching: this.generalLocationFlights.has(name) };
|
|
1074
|
+
}), machines: (this.runtimeHub?.listMachines() ?? []).filter(row => row.status !== "pending" && row.status !== "revoked").map(row => ({
|
|
1075
|
+
id: row.id, name: row.name, online: row.status === "online" && !!this.runtimeHub?.paneTransportEpoch(row.id),
|
|
1076
|
+
clis: row.inventory?.clis.filter(cli => cli.runnable && (cli.id === "claude-code" || cli.id === "codex")).map(cli => ({ id: cli.id, version: cli.version })) ?? [],
|
|
1077
|
+
})) };
|
|
1078
|
+
}
|
|
1079
|
+
/** Pure preflight for the confirmation diff and stale-tab refusal; never prepares files or starts a CLI. */
|
|
1080
|
+
previewGeneralLocation(raw) {
|
|
1081
|
+
const input = parseGeneralSwitchInput(raw), cfg = this.fleetConfig?.instances[input.name];
|
|
1082
|
+
if (!cfg?.general_topic || this.shuttingDown)
|
|
1083
|
+
throw Error("General is unavailable");
|
|
1084
|
+
const record = this.generalBindings().read(input.name);
|
|
1085
|
+
if (input.revision !== generalBindingRevision(record, cfg))
|
|
1086
|
+
throw Error("General location changed; read its settings again");
|
|
1087
|
+
const connections = cfg.channel_id ? [cfg.channel_id] : [];
|
|
1088
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1089
|
+
if (channels.length && (connections.length !== 1 || channels.filter(row => (row.id ?? row.type) === connections[0]).length !== 1))
|
|
1090
|
+
throw Error("General's connection cannot be resolved");
|
|
1091
|
+
if (input.target.location === "host") {
|
|
1092
|
+
if (!record)
|
|
1093
|
+
throw Error("General's saved host location is unavailable");
|
|
1094
|
+
return { input, connections, summary: [t("general_location.confirm_host"), t("general_location.history")] };
|
|
1095
|
+
}
|
|
1096
|
+
const active = record?.windows.find(row => row.id === record.active);
|
|
1097
|
+
if (!record?.pending && record?.hold !== "start" && active?.machineId === input.target.machineId && active.cli === input.target.cli)
|
|
1098
|
+
throw Error("General already runs at this location");
|
|
1099
|
+
const machine = this.runtimeHub?.machine(input.target.machineId);
|
|
1100
|
+
if (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(machine.id))
|
|
1101
|
+
throw Error(t("general_location.offline"));
|
|
1102
|
+
const provider = input.target.cli;
|
|
1103
|
+
if (!machine.inventory?.clis.some(cli => cli.id === provider && cli.runnable))
|
|
1104
|
+
throw Error("The chosen Windows CLI is unavailable");
|
|
1105
|
+
return { input, connections, summary: [t("general_location.confirm_windows", settingsDisplay(machine.name)), t("general_location.history")] };
|
|
1106
|
+
}
|
|
1107
|
+
/** All entry points, including automatic native recovery, must respect an interrupted binding switch. */
|
|
1108
|
+
assertGeneralBindingStart(name, config) {
|
|
1109
|
+
if (!config.general_topic)
|
|
1110
|
+
return;
|
|
1111
|
+
try {
|
|
1112
|
+
this.generalBindings().assertStart(name, config);
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
this.logger.warn({ name, err }, "General location start held by its saved binding");
|
|
1116
|
+
this.needsYou?.poke();
|
|
1117
|
+
throw new UnsupportedCliError(t("general_location.exit_unconfirmed"));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/** Only the Settings replay's confirmed admin can enter; the whole switch owns one lifecycle transition. */
|
|
1121
|
+
async switchGeneralLocation(raw, authorized, execution) {
|
|
1122
|
+
const { input } = this.previewGeneralLocation(raw), name = input.name;
|
|
1123
|
+
return this.lifecycle.runTransition(name, async (transition) => {
|
|
1124
|
+
execution.assert();
|
|
1125
|
+
if (!authorized() || this.generalLocationFlights.has(name))
|
|
1126
|
+
throw Error("General location authority expired or a switch is already running");
|
|
1127
|
+
const token = Symbol(name), generation = this.settingsGeneration;
|
|
1128
|
+
const destinationEpoch = input.target.location === "windows" ? this.runtimeHub?.paneTransportEpoch(input.target.machineId) : null;
|
|
1129
|
+
let expected = this.fleetConfig.instances[name], stopGeneration = this.explicitStopGeneration.get(name) ?? 0, epoch = this.lifecycle.epochOf(name);
|
|
1130
|
+
const hostSnapshot = !isWindowsInstance(expected) ? structuredClone(this.daemons.get(name)?.getConfigSnapshot() ?? expected) : undefined;
|
|
1131
|
+
const current = () => !this.shuttingDown && this.settingsGeneration === generation && authorized()
|
|
1132
|
+
&& this.generalLocationFlights.get(name) === token && this.fleetConfig?.instances[name] === expected
|
|
1133
|
+
&& (input.target.location !== "windows" || remoteRuntimeEnabled() && !!destinationEpoch && this.runtimeHub?.paneTransportEpoch(input.target.machineId) === destinationEpoch)
|
|
1134
|
+
&& (this.explicitStopGeneration.get(name) ?? 0) === stopGeneration && this.lifecycle.epochOf(name) === epoch;
|
|
1135
|
+
const check = () => { if (!current())
|
|
1136
|
+
throw Error("General location switch was superseded"); };
|
|
1137
|
+
this.generalLocationFlights.set(name, token);
|
|
1138
|
+
// Old ephemeral waiters carry this epoch; they cannot wake into the selected conversation.
|
|
1139
|
+
this.cancelPendingDeliveries(name);
|
|
1140
|
+
let lease = null;
|
|
1141
|
+
try {
|
|
1142
|
+
lease = await waitSettingsLease([settingsFileResource(this.getSettingsConfigPath()), `general:${this.dataDir}:${name}`], () => current() && execution.current(), performance.now() + execution.remainingMs, execution.owner);
|
|
1143
|
+
const store = this.generalBindings();
|
|
1144
|
+
const result = await switchGeneralLocation({ store, current: () => expected, assert: check,
|
|
1145
|
+
prepare: async (target, retained) => {
|
|
1146
|
+
if (target.location !== "windows")
|
|
1147
|
+
return null;
|
|
1148
|
+
const old = retained.windows.find(row => row.machineId === target.machineId && row.cli === target.cli);
|
|
1149
|
+
const id = old?.id ?? randomUUID(), agentId = old?.agentId ?? randomUUID();
|
|
1150
|
+
const publication = await this.prepareWindowsGeneralWorkspace(name, target.machineId, target.cli, id, () => current() && execution.current());
|
|
1151
|
+
check();
|
|
1152
|
+
execution.assert();
|
|
1153
|
+
const config = { ...structuredClone(expected), runtime: "windows", machine_id: target.machineId,
|
|
1154
|
+
native_adapter: "pane", native_agent_id: agentId, native_general_binding: id, native_sandbox: "workspace-write",
|
|
1155
|
+
backend: target.cli, model: target.model, working_directory: publication.cwd, systemPrompt: "", agent_mode: "cli" };
|
|
1156
|
+
// Host credentials, shell hooks and provider-specific options never become Windows launch configuration.
|
|
1157
|
+
for (const key of ["backend_options", "credential_profile", "pre_task_command"])
|
|
1158
|
+
delete config[key];
|
|
1159
|
+
windowsAgentsFor(this, this.runtimeWindowsExecutor()).ensureGeneralBinding({ id: agentId, instance: name, machineId: target.machineId,
|
|
1160
|
+
cli: target.cli, cwd: publication.cwd, name: (expected.display_name ?? name).slice(0, 64), model: target.model, instructions: "", description: expected.description ?? "" }, () => current() && execution.current());
|
|
1161
|
+
return { id, agentId, machineId: target.machineId, cli: target.cli, config };
|
|
1162
|
+
},
|
|
1163
|
+
idle: async () => {
|
|
1164
|
+
check();
|
|
1165
|
+
const daemon = this.daemons.get(name);
|
|
1166
|
+
return !daemon || daemon.getInstanceStateSnapshot().state === "idle" && daemon.getWorkingRecoveryState().idle
|
|
1167
|
+
&& daemon.getInteractionSnapshot().phase === "clear" && !this.lifecycle.hasWorkLease(name)
|
|
1168
|
+
&& Date.now() - (this.lastDeliveryAt.get(name) ?? 0) >= 2000;
|
|
1169
|
+
},
|
|
1170
|
+
hostSnapshot: () => hostSnapshot,
|
|
1171
|
+
capture: async () => {
|
|
1172
|
+
if (isWindowsInstance(expected))
|
|
1173
|
+
return undefined;
|
|
1174
|
+
const daemon = this.daemons.get(name);
|
|
1175
|
+
if (!daemon)
|
|
1176
|
+
throw Error(t("general_location.exit_unconfirmed"));
|
|
1177
|
+
const owner = await daemon.generalHostOwner();
|
|
1178
|
+
check();
|
|
1179
|
+
execution.assert();
|
|
1180
|
+
return owner;
|
|
1181
|
+
},
|
|
1182
|
+
stop: async (owner) => {
|
|
1183
|
+
check();
|
|
1184
|
+
const stopping = this.stopInstance(name, transition);
|
|
1185
|
+
stopGeneration++;
|
|
1186
|
+
epoch++;
|
|
1187
|
+
await stopping;
|
|
1188
|
+
check();
|
|
1189
|
+
if (isWindowsInstance(expected)) {
|
|
1190
|
+
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(expected.native_agent_id);
|
|
1191
|
+
if (profile.activeId || profile.paneLaunchId)
|
|
1192
|
+
throw Error(t("general_location.exit_unconfirmed"));
|
|
1193
|
+
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1194
|
+
return undefined;
|
|
1195
|
+
}
|
|
1196
|
+
if (!owner || !await confirmGeneralHostExit(owner, getTmuxSession()))
|
|
1197
|
+
throw Error(t("general_location.exit_unconfirmed"));
|
|
1198
|
+
check();
|
|
1199
|
+
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1200
|
+
return owner.pin;
|
|
1201
|
+
},
|
|
1202
|
+
save: next => {
|
|
1203
|
+
check();
|
|
1204
|
+
const previous = expected;
|
|
1205
|
+
try {
|
|
1206
|
+
this.fleetConfig.instances[name] = next;
|
|
1207
|
+
this.saveFleetConfig([{ path: ["instances", name], value: next }]);
|
|
1208
|
+
expected = next;
|
|
1209
|
+
this.routing.rebuild(this.fleetConfig);
|
|
1210
|
+
}
|
|
1211
|
+
catch (err) {
|
|
1212
|
+
this.fleetConfig.instances[name] = previous;
|
|
1213
|
+
throw err;
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
start: async () => {
|
|
1217
|
+
check();
|
|
1218
|
+
if (!execution.completed)
|
|
1219
|
+
throw Error("General's selected binding has no committed approval");
|
|
1220
|
+
// The committed receipt permits only this selected startup. Live authority still fences every awaited spawn boundary.
|
|
1221
|
+
const startup = new SettingsExecution({ current: () => current() && execution.completed && execution.remainingMs > 0,
|
|
1222
|
+
snapshot: () => this.fleetConfig });
|
|
1223
|
+
try {
|
|
1224
|
+
await this.startInstance(name, expected, true, "fleet-topic", false, transition, startup);
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
startup.close();
|
|
1228
|
+
}
|
|
1229
|
+
check();
|
|
1230
|
+
const daemon = this.daemons.get(name);
|
|
1231
|
+
if (!daemon || daemon.getProcessStatus() !== "running" || isNativePaneInstance(expected) && !daemon.nativePaneCurrent())
|
|
1232
|
+
throw Error(t("general_location.start_failed"));
|
|
1233
|
+
await this.connectIpcToInstance(name);
|
|
1234
|
+
check();
|
|
1235
|
+
},
|
|
1236
|
+
notify: (kind, err) => {
|
|
1237
|
+
const message = t(kind === "exit" ? "general_location.exit_unconfirmed" : kind === "start" ? "general_location.start_failed" : "general_location.complete", isWindowsInstance(expected) ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host"));
|
|
1238
|
+
this.eventLog?.insert(name, "general_location", { phase: kind, message });
|
|
1239
|
+
if (err)
|
|
1240
|
+
this.logger.warn({ name, phase: kind, err }, message);
|
|
1241
|
+
else
|
|
1242
|
+
this.logger.info({ name, phase: kind }, message);
|
|
1243
|
+
this.needsYou?.poke();
|
|
1244
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1245
|
+
this.notifyInstanceTopic(name, message);
|
|
1246
|
+
},
|
|
1247
|
+
}, input, execution);
|
|
1248
|
+
return { ...result, message: result.started ? t("general_location.complete", isWindowsInstance(expected)
|
|
1249
|
+
? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host")) : t("general_location.start_failed") };
|
|
1250
|
+
}
|
|
1251
|
+
finally {
|
|
1252
|
+
lease?.release();
|
|
1253
|
+
if (this.generalLocationFlights.get(name) === token)
|
|
1254
|
+
this.generalLocationFlights.delete(name);
|
|
1255
|
+
this.needsYou?.poke();
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
/** An offline PC or a confirmed binding boundary holds old waiting input; it never becomes another conversation's queue. */
|
|
1260
|
+
holdGeneralOldDeliveries(name) {
|
|
1261
|
+
const outbox = this.deliveryOutbox;
|
|
1262
|
+
if (!outbox)
|
|
1263
|
+
return;
|
|
1264
|
+
for (const row of outbox.listPending().filter(row => row.targetInstance === name)) {
|
|
1265
|
+
if (row.state !== "submission_started")
|
|
1266
|
+
outbox.failBeforeBegin(row.deliveryId, t("general_location.input_not_replayed"));
|
|
1267
|
+
else if (row.targetDaemonBootId) {
|
|
1268
|
+
outbox.markTargetReconciliationPending(name, row.targetDaemonBootId);
|
|
1269
|
+
if (!outbox.reconcileAttempt(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "uncertain", t("general_location.input_not_replayed")))
|
|
1270
|
+
throw Error("General's old delivery could not be held");
|
|
1271
|
+
}
|
|
1272
|
+
else
|
|
1273
|
+
throw Error("General's old delivery has no confirmed owner");
|
|
1274
|
+
}
|
|
1275
|
+
this.needsYou?.poke();
|
|
1276
|
+
}
|
|
1277
|
+
generalLocationDeliveryHold(name) {
|
|
1278
|
+
const config = this.fleetConfig?.instances[name];
|
|
1279
|
+
if (!config?.general_topic)
|
|
1280
|
+
return null;
|
|
1281
|
+
if (this.generalLocationFlights?.has(name))
|
|
1282
|
+
return t("general_location.exit_unconfirmed");
|
|
1283
|
+
let record;
|
|
1284
|
+
try {
|
|
1285
|
+
record = this.generalBindings().read(name);
|
|
1286
|
+
}
|
|
1287
|
+
catch {
|
|
1288
|
+
return t("general_location.start_failed");
|
|
1289
|
+
}
|
|
1290
|
+
if (record?.pending || record?.hold)
|
|
1291
|
+
return t(record.pending?.phase === "selected" || record.hold === "start" ? "general_location.start_failed" : "general_location.exit_unconfirmed");
|
|
1292
|
+
if (isWindowsInstance(config) && config.machine_id && (!remoteRuntimeEnabled() || this.runtimeHub?.machine(config.machine_id)?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(config.machine_id)))
|
|
1293
|
+
return t("general_location.offline");
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
generalLocationAttention(name) {
|
|
1297
|
+
const cfg = this.fleetConfig?.instances[name];
|
|
1298
|
+
if (!cfg?.general_topic)
|
|
1299
|
+
return null;
|
|
1300
|
+
const detail = this.generalLocationDeliveryHold(name);
|
|
1301
|
+
if (!detail) {
|
|
1302
|
+
this.generalAttentionEpisodes.delete(name);
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
let record = null;
|
|
1306
|
+
try {
|
|
1307
|
+
record = this.generalBindings().read(name);
|
|
1308
|
+
}
|
|
1309
|
+
catch { /* a corrupt journal is a visible hold too */ }
|
|
1310
|
+
const offline = !record?.pending && !record?.hold && isWindowsInstance(cfg) && detail === t("general_location.offline");
|
|
1311
|
+
const key = record?.pending?.id ?? `${record?.revision ?? "unknown"}:${offline ? "offline" : "hold"}`;
|
|
1312
|
+
let episode = this.generalAttentionEpisodes.get(name);
|
|
1313
|
+
if (!episode || episode.key !== key) {
|
|
1314
|
+
episode = { key, since: record?.pending?.at ?? Date.now() };
|
|
1315
|
+
this.generalAttentionEpisodes.set(name, episode);
|
|
1316
|
+
}
|
|
1317
|
+
return { ...episode, detail, offline };
|
|
1318
|
+
}
|
|
1319
|
+
async nativePaneFor(name, config, assertCurrent) {
|
|
1320
|
+
try {
|
|
1321
|
+
return await (await this.nativePanes()).prepare(name, config, assertCurrent);
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
if (config.general_topic && err instanceof GeneralManagedFileError) {
|
|
1325
|
+
assertCurrent();
|
|
1326
|
+
const store = this.generalBindings(), record = store.read(name);
|
|
1327
|
+
if (record && record.active === config.native_general_binding) {
|
|
1328
|
+
record.hold = "start";
|
|
1329
|
+
store.write(record);
|
|
1330
|
+
}
|
|
1331
|
+
this.logger.warn({ name, managedFile: err.file }, t("general_location.start_failed"));
|
|
1332
|
+
this.needsYou?.poke();
|
|
1333
|
+
throw new UnsupportedCliError(t("general_location.start_failed"));
|
|
1334
|
+
}
|
|
1335
|
+
throw err;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async recoverNativePane(name, launchId, authorized) {
|
|
1339
|
+
const config = this.fleetConfig?.instances[name];
|
|
1340
|
+
if (!isNativePaneInstance(config))
|
|
1341
|
+
throw Error("Persistent Windows pane is not configured");
|
|
1342
|
+
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1343
|
+
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1344
|
+
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1345
|
+
throw Error(t("native_pane.authority_expired")); };
|
|
1346
|
+
current();
|
|
1347
|
+
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1348
|
+
throw Error(t("native_pane.still_running"));
|
|
1349
|
+
this.cancelStartupRetry(name);
|
|
1350
|
+
this.lifecycle.cancelNativeRecovery(name);
|
|
1351
|
+
await (await this.nativePanes()).recover(name, config, launchId, true, current);
|
|
1352
|
+
current();
|
|
1353
|
+
// The host supplied a positive exit before ordinary lifecycle cleanup. No signal or input is required.
|
|
1354
|
+
await this.lifecycle.stop(name, transition);
|
|
1355
|
+
current();
|
|
1356
|
+
this.instanceProcessStatus.set(name, "stopped");
|
|
1357
|
+
this.eventLog?.insert(name, "native_pane_recovered", { launchId, confirmedStopped: true });
|
|
1358
|
+
this.logger.info({ name, launchId }, "Windows saved pane recovered by an administrator; waiting for Start");
|
|
1359
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
holdNativePaneRecovery(name, error) {
|
|
1363
|
+
if (!(error instanceof NativePaneRecoveryError))
|
|
1364
|
+
return false;
|
|
1365
|
+
this.instanceProcessStatus.set(name, "stopped");
|
|
1366
|
+
this.logger.warn({ name, recovery: error.recovery }, error.message);
|
|
1367
|
+
this.eventLog?.insert(name, "native_pane_recovery_needed", { ...error.recovery });
|
|
1368
|
+
this.needsYou?.poke();
|
|
1369
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
async startFreshNativePane(name, launchId, sessionId, authorized) {
|
|
1373
|
+
const config = this.fleetConfig?.instances[name];
|
|
1374
|
+
if (!isNativePaneInstance(config))
|
|
1375
|
+
throw Error("Persistent Windows pane is not configured");
|
|
1376
|
+
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1377
|
+
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1378
|
+
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1379
|
+
throw Error(t("native_pane.authority_expired")); };
|
|
1380
|
+
current();
|
|
1381
|
+
const held = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id).paneRecovery;
|
|
1382
|
+
if (held?.kind !== "session-unconfirmed" || held.launchId !== launchId || (held.sessionId ?? null) !== sessionId)
|
|
1383
|
+
throw Error(t("native_pane.changed"));
|
|
1384
|
+
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1385
|
+
throw Error(t("native_pane.still_running"));
|
|
1386
|
+
this.cancelStartupRetry(name);
|
|
1387
|
+
this.lifecycle.cancelNativeRecovery(name);
|
|
1388
|
+
await this.lifecycle.stop(name, transition);
|
|
1389
|
+
current();
|
|
1390
|
+
try {
|
|
1391
|
+
await (await this.nativePanes()).startFresh(name, config, launchId, sessionId, current);
|
|
1392
|
+
current();
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
current();
|
|
1396
|
+
this.needsYou?.poke();
|
|
1397
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1398
|
+
throw error;
|
|
1399
|
+
}
|
|
1400
|
+
this.eventLog?.insert(name, "native_pane_new_conversation", { launchId, sessionId });
|
|
1401
|
+
await this.startInstance(name, config, true, "fleet-topic", true, transition);
|
|
1402
|
+
current();
|
|
1403
|
+
this.needsYou?.poke();
|
|
1404
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
nativePanePinnedSession(name) {
|
|
1408
|
+
const config = this.fleetConfig?.instances[name];
|
|
1409
|
+
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
1410
|
+
return null;
|
|
1411
|
+
const agent = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
1412
|
+
return agent.fleet_instance === name && agent.machineId === config.machine_id ? agent.threadId ?? null : null;
|
|
1413
|
+
}
|
|
1414
|
+
onNativePaneRecoverySettled(name) {
|
|
1415
|
+
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1416
|
+
this.instanceProcessStatus.delete(name);
|
|
1417
|
+
this.onAttentionChanged(name);
|
|
1418
|
+
this.scheduleDeliveryOutboxPump();
|
|
1419
|
+
}
|
|
1420
|
+
setNativePanePaused(name, paused) {
|
|
1421
|
+
const id = this.fleetConfig?.instances[name]?.native_agent_id;
|
|
1422
|
+
if (!id)
|
|
1423
|
+
throw Error("Native profile is missing");
|
|
1424
|
+
windowsAgentsFor(this, this.runtimeWindowsExecutor()).setPaused(id, paused);
|
|
1425
|
+
}
|
|
1426
|
+
async closeRetainedNativePane(name) {
|
|
1427
|
+
const config = this.fleetConfig?.instances[name];
|
|
1428
|
+
if (!config)
|
|
1429
|
+
throw Error("Native instance configuration is missing");
|
|
1430
|
+
await (await this.nativePanes()).closeRetained(name, config, () => { if (this.shuttingDown)
|
|
1431
|
+
throw Error("Fleet is stopping"); });
|
|
1432
|
+
}
|
|
1433
|
+
async reconcileNativePaneBeforeStart(name, daemon) {
|
|
1434
|
+
const outbox = this.deliveryOutbox;
|
|
1435
|
+
if (!outbox)
|
|
1436
|
+
return true;
|
|
1437
|
+
const epoch = this.lifecycle.epochOf(name);
|
|
1438
|
+
return daemon.reconcileNativeDeliveries(outbox, () => !this.shuttingDown && this.lifecycle.epochOf(name) === epoch);
|
|
1439
|
+
}
|
|
1440
|
+
/** M-2255: the Windows runtimes' switches the fleet's own state reflects (null until the start-up has applied them). */
|
|
1441
|
+
runtimeEffects = null;
|
|
1442
|
+
runtimeEffectsScheduled = false;
|
|
1443
|
+
localPaneDrain = null;
|
|
1444
|
+
remotePaneDrain = null;
|
|
1445
|
+
/** Revoke admissions synchronously; keep the captured transport only long enough for exact owned closes. */
|
|
1446
|
+
drainNativeRuntime(remote) {
|
|
1447
|
+
const existing = remote ? this.remotePaneDrain : this.localPaneDrain;
|
|
1448
|
+
if (existing)
|
|
1449
|
+
return existing;
|
|
1450
|
+
const hub = this.runtimeHub, relay = this.localPaneRelay;
|
|
1451
|
+
if (!remote)
|
|
1452
|
+
relay?.beginCleanup();
|
|
1453
|
+
const targets = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, config]) => isWindowsInstance(config) && !!config.machine_id === remote);
|
|
1454
|
+
for (const [name, config] of targets) {
|
|
1455
|
+
this.nativeMcpStore?.closeInstance(name);
|
|
1456
|
+
if (isNativePaneInstance(config)) {
|
|
1457
|
+
this.lifecycle.cancelNativeRecovery(name);
|
|
1458
|
+
this.lifecycle.invalidate(name);
|
|
1459
|
+
this.daemons.get(name)?.fenceDeliveryWritesForStop();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const run = Promise.resolve().then(async () => {
|
|
1463
|
+
const results = await Promise.allSettled(targets.filter(([, config]) => isNativePaneInstance(config)).map(([name]) => this.stopInstance(name)));
|
|
1464
|
+
results.forEach((result, index) => { if (result.status === "rejected")
|
|
1465
|
+
this.logger.warn({ err: result.reason, remote, index }, "Windows pane cleanup unconfirmed; its reservation is retained"); });
|
|
1466
|
+
try {
|
|
1467
|
+
if (remote)
|
|
1468
|
+
await hub?.shutdownPaneHosts();
|
|
1469
|
+
else
|
|
1470
|
+
await relay?.shutdownHost();
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
this.logger.warn({ err: error, remote }, "Windows pane host shutdown unconfirmed; owned state is retained");
|
|
1474
|
+
}
|
|
1475
|
+
}).finally(() => {
|
|
1476
|
+
if (remote) {
|
|
1477
|
+
hub?.close();
|
|
1478
|
+
if (this.runtimeHub === hub)
|
|
1479
|
+
this.runtimeHub = null;
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
relay?.stop();
|
|
1483
|
+
if (this.localPaneRelay === relay)
|
|
1484
|
+
this.localPaneRelay = null;
|
|
1485
|
+
closeWindowsBridge(this);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
if (remote)
|
|
1489
|
+
this.remotePaneDrain = run;
|
|
1490
|
+
else
|
|
1491
|
+
this.localPaneDrain = run;
|
|
1492
|
+
void run.finally(() => {
|
|
1493
|
+
if (remote) {
|
|
1494
|
+
if (this.remotePaneDrain === run)
|
|
1495
|
+
this.remotePaneDrain = null;
|
|
1496
|
+
}
|
|
1497
|
+
else if (this.localPaneDrain === run)
|
|
1498
|
+
this.localPaneDrain = null;
|
|
1499
|
+
if (!this.shuttingDown && (remote ? remoteRuntimeEnabled() : localWindowsCliEnabled())) {
|
|
1500
|
+
this.initializeNativeInstances();
|
|
1501
|
+
this.initializeNativeInstances(true);
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1505
|
+
}
|
|
1506
|
+
catch { }
|
|
1507
|
+
}).catch(error => this.logger.warn({ err: error }, "Windows runtime cleanup failed; retained ownership requires reconciliation"));
|
|
1508
|
+
return run;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* M-2255: Settings → Runtime's switches (fleet.yaml `runtimes`, or their environment override) in force — set each time
|
|
1512
|
+
* the configuration in force changes (a load, a save, a reload's rollback), next to the web authority's. Every gate reads
|
|
1513
|
+
* them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
|
|
1514
|
+
* sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
|
|
1515
|
+
*/
|
|
1516
|
+
observeRuntimeSwitches() {
|
|
1517
|
+
setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
|
|
1518
|
+
if (!this.runtimeEffects || this.runtimeEffectsScheduled)
|
|
1519
|
+
return;
|
|
1520
|
+
this.runtimeEffectsScheduled = true;
|
|
1521
|
+
queueMicrotask(() => { this.runtimeEffectsScheduled = false; this.applyRuntimeSwitches(); });
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Turned off: what it allowed ends now — the paired computers' connections (their agents show as offline) or this PC's
|
|
1525
|
+
* Windows process (a turn in progress is cancelled, as when the fleet stops), and those agents' tool sessions.
|
|
1526
|
+
* Turned on: the Windows agents are registered and their chat history read, as at start-up.
|
|
1527
|
+
*/
|
|
1528
|
+
applyRuntimeSwitches() {
|
|
1529
|
+
const was = this.runtimeEffects;
|
|
1530
|
+
if (!was || this.shuttingDown)
|
|
1531
|
+
return;
|
|
1532
|
+
const now = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
1533
|
+
if (was.windows_cli === now.windows_cli && was.remote_runtime === now.remote_runtime)
|
|
1534
|
+
return;
|
|
1535
|
+
this.runtimeEffects = now;
|
|
1536
|
+
try {
|
|
1537
|
+
if (was.remote_runtime && !now.remote_runtime) {
|
|
1538
|
+
void this.drainNativeRuntime(true);
|
|
1539
|
+
}
|
|
1540
|
+
if (was.windows_cli && !now.windows_cli) {
|
|
1541
|
+
void this.drainNativeRuntime(false);
|
|
1542
|
+
}
|
|
1543
|
+
if (!was.windows_cli && !was.remote_runtime && !this.localPaneDrain && !this.remotePaneDrain) {
|
|
1544
|
+
this.initializeNativeInstances();
|
|
1545
|
+
this.initializeNativeInstances(true);
|
|
1546
|
+
}
|
|
1547
|
+
this.logger.info({ windows_cli: now.windows_cli, remote_runtime: now.remote_runtime }, "Windows runtime switches applied");
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
this.logger.warn({ err: error }, "Windows runtime switches could not be applied in full");
|
|
1551
|
+
}
|
|
1552
|
+
try {
|
|
1553
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
1554
|
+
}
|
|
1555
|
+
catch { /* the next status frame carries it */ }
|
|
1556
|
+
}
|
|
1557
|
+
/** An unavailable opt-in runtime must not prevent the Linux fleet from starting or restoring its own history. */
|
|
1558
|
+
initializeNativeInstances(history = false) {
|
|
1559
|
+
if (!windowsCliEnabled())
|
|
1560
|
+
return;
|
|
1561
|
+
try {
|
|
1562
|
+
const runtime = this.nativeInstanceRuntime();
|
|
1563
|
+
if (history)
|
|
1564
|
+
runtime.importHistory();
|
|
1565
|
+
else
|
|
1566
|
+
runtime.register();
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
this.logger.warn({ err: error }, "Windows runtime state could not be loaded; affected native instances remain unavailable");
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/** Native CLIs are headless: the manager owns the durable delivery generation instead of a tmux daemon. */
|
|
1573
|
+
deliveryBootId(name) {
|
|
1574
|
+
const config = this.fleetConfig?.instances[name];
|
|
1575
|
+
if (isNativeExecInstance(config)) {
|
|
1576
|
+
if (!this.nativeInstanceRuntime().available(name))
|
|
1577
|
+
return null;
|
|
1578
|
+
const machine = config.machine_id ? this.runtimeCoordinator().machine(config.machine_id) : null;
|
|
1579
|
+
return machine ? `runtime:${machine.id}:${machine.bootId ?? machine.generation}` : this.managerBootId;
|
|
1580
|
+
}
|
|
1581
|
+
return this.daemons.get(name)?.bootId ?? null;
|
|
1582
|
+
}
|
|
740
1583
|
rawFleetConfig = {};
|
|
741
1584
|
rawFleetDocument = null;
|
|
742
1585
|
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
@@ -1536,6 +2379,8 @@ export class FleetManager {
|
|
|
1536
2379
|
* authorizing revoked links and cookies until the fleet restarted.
|
|
1537
2380
|
*/
|
|
1538
2381
|
settingsGeneration = 0;
|
|
2382
|
+
/** #2942: the settings baseline the agent-requested Windows changes are checked against (set with the gate). */
|
|
2383
|
+
settingsBaselineReader = null;
|
|
1539
2384
|
settingsRuntimeRevision = 0;
|
|
1540
2385
|
settingsConfirmation = null;
|
|
1541
2386
|
settingsControl = null;
|
|
@@ -1592,8 +2437,9 @@ export class FleetManager {
|
|
|
1592
2437
|
return this.settingsConfirmation;
|
|
1593
2438
|
const generation = this.settingsGeneration;
|
|
1594
2439
|
const current = () => !this.shuttingDown && this.settingsGeneration === generation;
|
|
1595
|
-
const baselines = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
|
|
2440
|
+
const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
|
|
1596
2441
|
config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
|
|
2442
|
+
generalLocation: input => this.previewGeneralLocation(input),
|
|
1597
2443
|
proof: (kind, target, id, binding, key) => {
|
|
1598
2444
|
const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
|
|
1599
2445
|
const proof = map.get(id);
|
|
@@ -1614,6 +2460,17 @@ export class FleetManager {
|
|
|
1614
2460
|
const store = new SettingsConfirmationStore({
|
|
1615
2461
|
audit: (event, fields) => this.logger.info({ event: `settings_confirmation.${event}`, ...fields }, "Settings confirmation audit"),
|
|
1616
2462
|
notify: view => view.state === "pending" ? this.promptSettingsChange(store, view) : Promise.resolve(),
|
|
2463
|
+
// alpha.2 (M-2300): an agent's request came, was decided or ran out — the open pages read the list again (a ping
|
|
2464
|
+
// with nothing in it: only a web-only fleet's verified owner may read it, GET /ui/agent-proposals).
|
|
2465
|
+
changed: view => {
|
|
2466
|
+
if (view.source !== "agent_request")
|
|
2467
|
+
return;
|
|
2468
|
+
this.agentProposalsRev++;
|
|
2469
|
+
broadcastSseEvent(this.sseClients, "agent_proposals", {}, err => this.logger.debug({ err }, "agent_proposals event failed"));
|
|
2470
|
+
// …and a status frame with the new revision: a tab from before the upgrade (its stream knows no `agent_proposals`)
|
|
2471
|
+
// applies status frames, and a polling page reads the same frame at its next /ui/poll.
|
|
2472
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
2473
|
+
},
|
|
1617
2474
|
});
|
|
1618
2475
|
const gate = new SettingsHttpConfirmation(store, {
|
|
1619
2476
|
principal: req => {
|
|
@@ -1661,8 +2518,10 @@ export class FleetManager {
|
|
|
1661
2518
|
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
1662
2519
|
if (auth.kind !== "ok")
|
|
1663
2520
|
return null;
|
|
1664
|
-
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
1665
|
-
|
|
2521
|
+
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there. #3008: except
|
|
2522
|
+
// this computer's admin signed in from outside (the authenticator's code, over the link connections from outside
|
|
2523
|
+
// opened): the web-only fleet's owner, judged below by the same authority as here (hostOperator).
|
|
2524
|
+
if (gatewayRequestContext(req) && auth.session.remote !== "totp")
|
|
1666
2525
|
return null;
|
|
1667
2526
|
// …and never a change that turns the named-link option on: that one needs a General's card or the host.
|
|
1668
2527
|
if (!store.selfConfirmable(id))
|
|
@@ -1695,30 +2554,97 @@ export class FleetManager {
|
|
|
1695
2554
|
if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
|
|
1696
2555
|
return null;
|
|
1697
2556
|
const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
|
|
1698
|
-
|
|
2557
|
+
const destinations = this.settingsNoticeGenerals(affected, chat.platform);
|
|
2558
|
+
if (!destinations.length)
|
|
1699
2559
|
return null;
|
|
1700
2560
|
const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
|
|
1701
2561
|
return {
|
|
1702
2562
|
label: t("settings_confirm.web_verified_label", name),
|
|
1703
2563
|
// admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
|
|
1704
|
-
current: () => !!verified() && chat.admin(actor.user_id) &&
|
|
2564
|
+
current: () => !!verified() && chat.admin(actor.user_id) && destinations.some(route => route.current()),
|
|
2565
|
+
followStop: async (name, begin) => { const route = destinations.find(item => item.current()); if (!route)
|
|
2566
|
+
throw Error("Settings route expired"); await route.followStop(name, begin); },
|
|
1705
2567
|
done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
|
|
1706
2568
|
};
|
|
1707
2569
|
}
|
|
1708
|
-
/**
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2570
|
+
/**
|
|
2571
|
+
* alpha.2 (M-2300; #2973's agent requests, junsion's §6): on a fleet with no chat connection there is no General to
|
|
2572
|
+
* post an agent's Windows request in — it waited for `agend settings confirm` on the host. Its owner (decision
|
|
2573
|
+
* 28c2d3a3: the `agend web --code` session on this computer, web-verified) now sees and decides it from the web's
|
|
2574
|
+
* Needs you. The authority is asked from the request each time, and again at execution by the store (`current`):
|
|
2575
|
+
* still that owner now (this computer, not revoked, signed out or run out), still a fleet with no chat connection,
|
|
2576
|
+
* never over the public link. Anyone else — a guest, a LAN or public-link session, a chat-connected fleet's admin —
|
|
2577
|
+
* gets nothing (a chat fleet keeps General's card as its way).
|
|
2578
|
+
*/
|
|
2579
|
+
/**
|
|
2580
|
+
* #2997 review (Prism P2): how many times an agent's request came or was decided — in a web-only fleet's status frame,
|
|
2581
|
+
* so a page that never hears the stream's ping (one polling /ui/poll, one reconnecting, a tab from before the upgrade)
|
|
2582
|
+
* reads the list again when it moves. A count, nothing more.
|
|
2583
|
+
*/
|
|
2584
|
+
agentProposalsRev = 0;
|
|
2585
|
+
agentProposalOwner(req) {
|
|
2586
|
+
// #3008: this computer's admin here, or signed in from outside (over the link connections from outside opened) —
|
|
2587
|
+
// asked of the LIVE session each time (#2997 review): a sign-out, an expiry, a revocation or the switch going off
|
|
2588
|
+
// while the request is applied fences it.
|
|
2589
|
+
return this.liveOwner(req, { hostOnly: false });
|
|
2590
|
+
}
|
|
2591
|
+
/** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
|
|
2592
|
+
webAgentProposals(req) {
|
|
2593
|
+
if (!this.agentProposalOwner(req))
|
|
2594
|
+
return null;
|
|
2595
|
+
return this.settingsGate().store.listBySource("agent_request");
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* alpha.2: the owner's Approve or Reject of an agent's request. The store claims it before its first await (a second
|
|
2599
|
+
* click, from here or the host, finds it no longer pending) and asks the owner, the request (its fleet generation and
|
|
2600
|
+
* Windows switch) and the target machine again before and while it runs the same creation code the web uses. The
|
|
2601
|
+
* answer is always the request as it now stands: applied (with the new agent's name), rejected, expired, stale or
|
|
2602
|
+
* failed (with its reason), or already decided.
|
|
2603
|
+
*/
|
|
2604
|
+
async webDecideAgentProposal(req, id, decision) {
|
|
2605
|
+
const owner = this.agentProposalOwner(req);
|
|
2606
|
+
if (!owner)
|
|
2607
|
+
return { status: 403, body: { error: "owner_required", message: t("settings_confirm.web_owner_required") } };
|
|
2608
|
+
const store = this.settingsGate().store, before = store.inspect(id);
|
|
2609
|
+
if (!before || before.source !== "agent_request")
|
|
2610
|
+
return { status: 404, body: { error: "not_found", message: t("settings_confirm.no_longer_pending") } };
|
|
2611
|
+
try {
|
|
2612
|
+
const view = await store.decide(id, decision, { ...owner, via: "web" });
|
|
2613
|
+
this.logger.info({ id, decision, state: view.state }, "Agent request decided from the web by this computer's admin");
|
|
2614
|
+
return { status: 200, body: { view, agent: agentNameOf(view) } };
|
|
2615
|
+
}
|
|
2616
|
+
catch (err) {
|
|
2617
|
+
const code = err instanceof SettingsConfirmationError ? err.code : "decide_failed";
|
|
2618
|
+
const status = err instanceof SettingsConfirmationError ? err.status : 500;
|
|
2619
|
+
// Already decided (a double click, the host's command), run out or withdrawn: say what it is now.
|
|
2620
|
+
return { status, body: { error: code, message: t(code === "admin_required" ? "settings_confirm.web_owner_required" : "settings_confirm.no_longer_pending"), view: store.inspect(id) } };
|
|
1720
2621
|
}
|
|
1721
|
-
|
|
2622
|
+
}
|
|
2623
|
+
/** A confirmation belongs to the server route/lifetime, not the availability of General's model. */
|
|
2624
|
+
settingsGeneralRoute(name, platform, excluded) {
|
|
2625
|
+
const config = this.fleetConfig?.instances[name];
|
|
2626
|
+
if (!config?.general_topic || this.shuttingDown || this.ipcStoppingInstances.has(name))
|
|
2627
|
+
return null;
|
|
2628
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
2629
|
+
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform)
|
|
2630
|
+
return null;
|
|
2631
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
2632
|
+
if (!chatId || !topic)
|
|
2633
|
+
return null;
|
|
2634
|
+
const route = () => !this.shuttingDown && this.fleetConfig?.instances[name]?.general_topic === true
|
|
2635
|
+
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
2636
|
+
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
2637
|
+
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic;
|
|
2638
|
+
const lifetime = new SettingsRouteLifetime(name, route, () => this.lifecycle.epochOf(name), () => this.ipcStoppingInstances.has(name));
|
|
2639
|
+
return lifetime.current() ? { adapterId, adapter, chatId, topic, current: lifetime.current, followStop: lifetime.followStop.bind(lifetime) } : null;
|
|
2640
|
+
}
|
|
2641
|
+
/** Connected, same-platform, unaffected server routes can post cards even while the model is offline. */
|
|
2642
|
+
settingsNoticeGenerals(affected, platform) {
|
|
2643
|
+
const excluded = new Set(affected);
|
|
2644
|
+
return Object.keys(this.fleetConfig?.instances ?? {}).flatMap(name => {
|
|
2645
|
+
const route = this.settingsGeneralRoute(name, platform, excluded);
|
|
2646
|
+
return route ? [route] : [];
|
|
2647
|
+
});
|
|
1722
2648
|
}
|
|
1723
2649
|
/**
|
|
1724
2650
|
* #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
|
|
@@ -1777,6 +2703,119 @@ export class FleetManager {
|
|
|
1777
2703
|
return { platform, current, admin: userId => current() && !!userId
|
|
1778
2704
|
&& identities.every(owner => this.isFleetAdmin(userId, owner.id)) };
|
|
1779
2705
|
}
|
|
2706
|
+
/**
|
|
2707
|
+
* #2942: a Windows create or settings change an AGENT asked for. Nothing is created or written here: it becomes a
|
|
2708
|
+
* pending settings change whose General card a fleet admin must confirm (promptSettingsChange — the same admin rule
|
|
2709
|
+
* as every card). Its apply runs the same creation/patch code the web uses, inside the store's SettingsExecution, with
|
|
2710
|
+
* the confirming admin, the Windows switch and the target machine asked again at that moment.
|
|
2711
|
+
*/
|
|
2712
|
+
async proposeAgentWindowsChange(input) {
|
|
2713
|
+
const gate = this.settingsGate(), baselines = this.settingsBaselineReader;
|
|
2714
|
+
if (!baselines || !this.fleetConfig)
|
|
2715
|
+
throw new Error("Fleet configuration is unavailable");
|
|
2716
|
+
const generation = this.settingsGeneration, machineId = input.machineId;
|
|
2717
|
+
const switchOn = () => machineId ? remoteRuntimeEnabled() : localWindowsCliEnabled();
|
|
2718
|
+
const online = () => !machineId || (remoteRuntimeEnabled() && this.runtimeCoordinator().machine(machineId)?.status === "online");
|
|
2719
|
+
if (!switchOn())
|
|
2720
|
+
throw new Error(machineId ? "Remote runtime is not enabled" : LOCAL_WINDOWS_CLI_OFF);
|
|
2721
|
+
const baseline = await baselines.read(), snapshot = settingsFingerprint(baselines.snapshot());
|
|
2722
|
+
const { view } = gate.store.propose({ session: `agent:${input.requester}`, key: `agent-windows:${input.key}`,
|
|
2723
|
+
requestFingerprint: input.fingerprint, fingerprint: input.fingerprint, section: "channel", requestedBy: input.requester,
|
|
2724
|
+
source: "agent_request", summary: input.summary, bytes: input.bytes, authority: input.authority, affectedConnections: [],
|
|
2725
|
+
current: () => !this.shuttingDown && this.settingsGeneration === generation && switchOn(),
|
|
2726
|
+
snapshot: () => baselines.snapshot(),
|
|
2727
|
+
unchanged: async () => (await baselines.read()).fingerprint === baseline.fingerprint && settingsFingerprint(baselines.snapshot()) === snapshot,
|
|
2728
|
+
// The store's own fence (this confirming admin, the session, the state) plus the switch and machine now.
|
|
2729
|
+
apply: async (actorCurrent, execution) => {
|
|
2730
|
+
execution.assert();
|
|
2731
|
+
// alpha.2 polish: a paired machine whose CLI inventory has not arrived yet (just paired) is asked for its own
|
|
2732
|
+
// report first (it runs on that PC), so the creation validates against what is installed there — never a refusal
|
|
2733
|
+
// that only a manual Refresh would undo. Its answer or failure changes nothing else; the fence is asked again.
|
|
2734
|
+
if (machineId && online() && !inventoryProbed(this.runtimeCoordinator().machine(machineId)?.inventory)) {
|
|
2735
|
+
// `force`: the runner's cached answer on a fresh profile is the placeholder itself (#3005 rig recheck).
|
|
2736
|
+
try {
|
|
2737
|
+
await this.runtimeCoordinator().call(machineId, "probe", { force: true });
|
|
2738
|
+
}
|
|
2739
|
+
catch { /* validation says what is missing */ }
|
|
2740
|
+
execution.assert();
|
|
2741
|
+
}
|
|
2742
|
+
return input.apply(() => actorCurrent() && online(), execution);
|
|
2743
|
+
} });
|
|
2744
|
+
return view;
|
|
2745
|
+
}
|
|
2746
|
+
/** #2942: create_instance with runtime "windows" from an agent: validated like the web, then held for General. */
|
|
2747
|
+
async requestWindowsCreate(requester, rawArgs) {
|
|
2748
|
+
const cfg = this.fleetConfig;
|
|
2749
|
+
if (!cfg)
|
|
2750
|
+
throw new Error("Fleet configuration is unavailable");
|
|
2751
|
+
let parsed;
|
|
2752
|
+
try {
|
|
2753
|
+
parsed = parseNativeInstanceCreate(rawArgs);
|
|
2754
|
+
}
|
|
2755
|
+
catch {
|
|
2756
|
+
throw new Error("Invalid Windows agent request: check the directory, sandbox, backend and model");
|
|
2757
|
+
}
|
|
2758
|
+
// The web's own projection: the same validation, topic target and diff the admin's dashboard confirmation shows.
|
|
2759
|
+
let diff;
|
|
2760
|
+
try {
|
|
2761
|
+
diff = prepareSettingsEffect("POST", "/ui/instances", rawArgs, { config: cfg, classic: {} }).diff;
|
|
2762
|
+
}
|
|
2763
|
+
catch {
|
|
2764
|
+
throw new Error("Invalid Windows agent request: its topic connection is not one this fleet can use");
|
|
2765
|
+
}
|
|
2766
|
+
if (!diff)
|
|
2767
|
+
throw new Error("Invalid Windows agent request");
|
|
2768
|
+
return this.proposeAgentWindowsChange({ requester, key: diff.fingerprint, fingerprint: diff.fingerprint, summary: diff.summary,
|
|
2769
|
+
machineId: parsed.input.machineId, bytes: JSON.stringify(rawArgs).length,
|
|
2770
|
+
// The General card is the confirmation for an agent's request, whatever the requested connection (a topic or none).
|
|
2771
|
+
authority: { connections: diff.authority?.connections ?? [], primaryGeneral: true, unknown: false },
|
|
2772
|
+
apply: (allowed, execution) => createNativeWindowsInstance(this, parsed, allowed, execution) });
|
|
2773
|
+
}
|
|
2774
|
+
/** #2942: update_instance_config on a Windows agent: only the fields the web editor allows, held for General. */
|
|
2775
|
+
async requestWindowsConfig(requester, name, patch) {
|
|
2776
|
+
const inst = this.fleetConfig?.instances[name];
|
|
2777
|
+
if (!isWindowsInstance(inst))
|
|
2778
|
+
throw new Error("Windows agent not found");
|
|
2779
|
+
let edit;
|
|
2780
|
+
try {
|
|
2781
|
+
edit = NativeInstanceConfigEdit.parse(patch);
|
|
2782
|
+
}
|
|
2783
|
+
catch {
|
|
2784
|
+
throw new Error("Invalid Windows agent settings: display_name, description and model only");
|
|
2785
|
+
}
|
|
2786
|
+
const before = { display_name: inst.display_name ?? "", description: inst.description ?? "", model: inst.model ?? "" };
|
|
2787
|
+
const changed = Object.keys(edit).filter(key => edit[key] !== before[key]);
|
|
2788
|
+
if (!changed.length)
|
|
2789
|
+
throw new Error("Nothing to change: these settings already have these values");
|
|
2790
|
+
const summary = changed.map(key => `${name} ${key}: ${settingsDisplay(before[key])} → ${settingsDisplay(edit[key])}`);
|
|
2791
|
+
const machineId = typeof inst.machine_id === "string" ? inst.machine_id : undefined;
|
|
2792
|
+
// #2973 review (Prism P1): a persistent pane is bound to a chat connection; changing it (a model change restarts it)
|
|
2793
|
+
// needs that connection's admins as well as the primary General's, exactly as its creation did. An instance whose
|
|
2794
|
+
// binding names no single configured connection is refused (fail closed), never confirmed by the General alone.
|
|
2795
|
+
const connections = [];
|
|
2796
|
+
if (isNativePaneInstance(inst) || inst.channel_id !== undefined) {
|
|
2797
|
+
const bound = inst.channel_id, channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
2798
|
+
if (typeof bound !== "string" || channels.filter(ch => (ch.id ?? ch.type) === bound).length !== 1)
|
|
2799
|
+
throw new Error("This Windows agent's chat connection cannot be resolved; change its settings in the web dashboard");
|
|
2800
|
+
connections.push(bound);
|
|
2801
|
+
}
|
|
2802
|
+
return this.proposeAgentWindowsChange({ requester, key: `${name}:${settingsFingerprint(edit)}`, fingerprint: settingsFingerprint([name, edit]),
|
|
2803
|
+
summary, machineId, bytes: JSON.stringify(edit).length, authority: { connections, primaryGeneral: true, unknown: false },
|
|
2804
|
+
// #2973 review (Prism P2): the update may wait for its lifecycle turn; if the agent is rebound meanwhile (B → C), the
|
|
2805
|
+
// approval for B must not stop or change it on C. Pin the approved target and require the execution's snapshot to be
|
|
2806
|
+
// unchanged before every pre-commit effect; the post-commit restart keeps its own (refreshed) checks.
|
|
2807
|
+
apply: (allowed, execution) => {
|
|
2808
|
+
const pinned = { channel: inst.channel_id, machine: inst.machine_id, adapter: inst.native_adapter, runtime: inst.runtime };
|
|
2809
|
+
const sameTarget = () => {
|
|
2810
|
+
const now = this.fleetConfig?.instances[name];
|
|
2811
|
+
return !!now && now.channel_id === pinned.channel && now.machine_id === pinned.machine && now.native_adapter === pinned.adapter && now.runtime === pinned.runtime;
|
|
2812
|
+
};
|
|
2813
|
+
// Before the commit: the exact target and an unchanged snapshot. After runtime.update completes the transaction,
|
|
2814
|
+
// current() is false by design; the committed operation's restart receipt then needs the same actor, target and
|
|
2815
|
+
// switch checks plus `completed` — a completed execution can never authorize another write (mutate refuses).
|
|
2816
|
+
return updateNativeWindowsInstance(this, name, edit, () => allowed() && sameTarget() && (execution.completed || execution.current()), execution);
|
|
2817
|
+
} });
|
|
2818
|
+
}
|
|
1780
2819
|
async promptSettingsChange(store, view) {
|
|
1781
2820
|
const generation = this.settingsGeneration;
|
|
1782
2821
|
const authority = this.settingsChatAuthority(store, view.id);
|
|
@@ -1785,25 +2824,15 @@ export class FleetManager {
|
|
|
1785
2824
|
return;
|
|
1786
2825
|
}
|
|
1787
2826
|
const excluded = new Set(store.affectedConnections(view.id));
|
|
1788
|
-
for (const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1792
|
-
const general = this.daemons.get(name);
|
|
1793
|
-
if (!adapterId || !adapter || !general || excluded.has(adapterId) || adapter.type !== authority.platform)
|
|
1794
|
-
continue;
|
|
1795
|
-
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1796
|
-
if (!chatId || !topic)
|
|
2827
|
+
for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
|
|
2828
|
+
const route = this.settingsGeneralRoute(name, authority.platform, excluded);
|
|
2829
|
+
if (!route)
|
|
1797
2830
|
continue;
|
|
2831
|
+
const { adapterId, adapter, chatId, topic } = route;
|
|
1798
2832
|
let retired = false, attached = false;
|
|
1799
2833
|
const postingDeadline = performance.now() + Math.min(5000, view.remaining_ms);
|
|
1800
|
-
const current = () => !retired && (attached || performance.now() < postingDeadline) &&
|
|
1801
|
-
&&
|
|
1802
|
-
&& this.daemons.get(name) === general && !this.ipcStoppingInstances.has(name)
|
|
1803
|
-
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
1804
|
-
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
1805
|
-
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic
|
|
1806
|
-
&& authority.current();
|
|
2834
|
+
const current = () => !retired && (attached || performance.now() < postingDeadline) && this.settingsGeneration === generation
|
|
2835
|
+
&& route.current() && authority.current();
|
|
1807
2836
|
if (!current())
|
|
1808
2837
|
continue;
|
|
1809
2838
|
try {
|
|
@@ -1813,7 +2842,7 @@ export class FleetManager {
|
|
|
1813
2842
|
t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
|
|
1814
2843
|
choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
|
|
1815
2844
|
expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
|
|
1816
|
-
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
|
|
2845
|
+
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId), confirmationStop: route.followStop },
|
|
1817
2846
|
});
|
|
1818
2847
|
void posted.then(nonce => {
|
|
1819
2848
|
if (!retired)
|
|
@@ -2081,6 +3110,7 @@ export class FleetManager {
|
|
|
2081
3110
|
try {
|
|
2082
3111
|
const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
|
|
2083
3112
|
label: t("settings_confirm.admin_label", data.userId ?? ""),
|
|
3113
|
+
followStop: entry.confirmationStop,
|
|
2084
3114
|
current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
|
|
2085
3115
|
&& !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
|
|
2086
3116
|
});
|
|
@@ -2455,12 +3485,15 @@ export class FleetManager {
|
|
|
2455
3485
|
}
|
|
2456
3486
|
/** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
|
|
2457
3487
|
admitDurableDelivery(input) {
|
|
3488
|
+
const bindingHold = this.generalLocationDeliveryHold(input.targetInstance);
|
|
3489
|
+
if (bindingHold)
|
|
3490
|
+
throw Error(bindingHold);
|
|
2458
3491
|
const outbox = this.deliveryOutbox;
|
|
2459
3492
|
if (!outbox)
|
|
2460
3493
|
throw new Error("Durable delivery store is unavailable");
|
|
2461
|
-
const
|
|
2462
|
-
const sourceDaemonBootId = input.sourceDaemonBootId ??
|
|
2463
|
-
if (!
|
|
3494
|
+
const currentSourceBootId = this.getDaemonBootId(input.sourceInstance);
|
|
3495
|
+
const sourceDaemonBootId = input.sourceDaemonBootId ?? currentSourceBootId;
|
|
3496
|
+
if (!currentSourceBootId || !sourceDaemonBootId || currentSourceBootId !== sourceDaemonBootId) {
|
|
2464
3497
|
throw new Error("Source daemon generation is no longer current; outcome is unknown");
|
|
2465
3498
|
}
|
|
2466
3499
|
const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
|
|
@@ -2494,7 +3527,7 @@ export class FleetManager {
|
|
|
2494
3527
|
const human = (item) => {
|
|
2495
3528
|
if (!item.authority || !state)
|
|
2496
3529
|
return item;
|
|
2497
|
-
const authority =
|
|
3530
|
+
const authority = this.receiptAuthorityNow(item.authority, state);
|
|
2498
3531
|
return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
|
|
2499
3532
|
};
|
|
2500
3533
|
// #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
|
|
@@ -2523,6 +3556,11 @@ export class FleetManager {
|
|
|
2523
3556
|
return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
|
|
2524
3557
|
}
|
|
2525
3558
|
getDaemonBootId(instanceName) {
|
|
3559
|
+
if (this.generalLocationDeliveryHold(instanceName))
|
|
3560
|
+
return undefined;
|
|
3561
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
3562
|
+
return !this.shuttingDown && windowsCliEnabled() && this.nativeMcpStore?.hasActive(instanceName) ? this.managerBootId : undefined;
|
|
3563
|
+
}
|
|
2526
3564
|
return this.daemons.get(instanceName)?.bootId;
|
|
2527
3565
|
}
|
|
2528
3566
|
markDurableResponseDelivered(sourceInstance, operationId) {
|
|
@@ -2531,8 +3569,22 @@ export class FleetManager {
|
|
|
2531
3569
|
/** Called after a replacement Daemon object is installed in lifecycle.daemons. */
|
|
2532
3570
|
onDaemonReady(name, daemonBootId) {
|
|
2533
3571
|
const daemon = this.daemons.get(name);
|
|
2534
|
-
if (daemon?.bootId === daemonBootId)
|
|
3572
|
+
if (daemon?.bootId === daemonBootId) {
|
|
2535
3573
|
this.listenForWorkingState(name, daemon);
|
|
3574
|
+
const cfg = this.fleetConfig?.instances[name];
|
|
3575
|
+
if (cfg?.general_topic && !this.generalLocationFlights.has(name) && daemon.getProcessStatus() === "running"
|
|
3576
|
+
&& (!isNativePaneInstance(cfg) || daemon.nativePaneCurrent())) {
|
|
3577
|
+
try {
|
|
3578
|
+
if (this.generalBindings().completeSelected(name, cfg)) {
|
|
3579
|
+
this.needsYou?.poke();
|
|
3580
|
+
this.logger.info({ name }, "General location's selected start receipt settled after recovery");
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
catch (err) {
|
|
3584
|
+
this.logger.warn({ name, err }, "General location start receipt remains held");
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
2536
3588
|
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
2537
3589
|
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
2538
3590
|
this.scheduleDeliveryOutboxPump();
|
|
@@ -2676,6 +3728,15 @@ export class FleetManager {
|
|
|
2676
3728
|
this.deliveryPumpRunning = true;
|
|
2677
3729
|
this.deliveryPumpScheduled = false;
|
|
2678
3730
|
try {
|
|
3731
|
+
// Native workers have no resident daemon to wake. Pending durable work resumes their logical instance;
|
|
3732
|
+
// the Windows bridge still owns the single physical execution slot.
|
|
3733
|
+
for (const target of new Set(outbox.listPending().map(row => row.targetInstance))) {
|
|
3734
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[target])) {
|
|
3735
|
+
const runtime = this.nativeInstanceRuntime(), agent = runtime.get(target);
|
|
3736
|
+
if (agent?.paused && !agent.blocked)
|
|
3737
|
+
runtime.setPaused(target, false);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
2679
3740
|
outbox.expireStale(undefined, undefined, target => {
|
|
2680
3741
|
const failure = this.wakeCoordinator?.wakeFailure(target);
|
|
2681
3742
|
return failure ? `target could not be woken (${failure})` : undefined;
|
|
@@ -2688,7 +3749,7 @@ export class FleetManager {
|
|
|
2688
3749
|
// Phase 2c: a lane a queue worker owns is never claimed here.
|
|
2689
3750
|
target => this.queueWorkers.has(target) || this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target)
|
|
2690
3751
|
? null
|
|
2691
|
-
: this.
|
|
3752
|
+
: this.deliveryBootId(target), this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
|
|
2692
3753
|
if (!claimed)
|
|
2693
3754
|
break;
|
|
2694
3755
|
this.activeDurableTargets.add(claimed.targetInstance);
|
|
@@ -2749,7 +3810,7 @@ export class FleetManager {
|
|
|
2749
3810
|
wanted: () => this.deliveryWorkerMode(target) === "on",
|
|
2750
3811
|
available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
|
|
2751
3812
|
blocked: () => this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target) === true,
|
|
2752
|
-
daemonBootId: () => this.
|
|
3813
|
+
daemonBootId: () => this.deliveryBootId(target),
|
|
2753
3814
|
claim: bootId => this.deliveryOutbox?.claimNext(this.managerBootId, candidate => candidate === target ? bootId : null, new Set(), row => this.recoveryDeliveryEligible(row)),
|
|
2754
3815
|
tryAcquireBudget: () => {
|
|
2755
3816
|
if (this.activeDurableTargets.size >= 8)
|
|
@@ -2780,6 +3841,10 @@ export class FleetManager {
|
|
|
2780
3841
|
const outbox = this.deliveryOutbox;
|
|
2781
3842
|
if (!outbox)
|
|
2782
3843
|
return;
|
|
3844
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[claimed.targetInstance])) {
|
|
3845
|
+
await this.dispatchNativeDurableDelivery(claimed);
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
2783
3848
|
const target = claimed.targetInstance;
|
|
2784
3849
|
const attempt = claimed.attemptNo;
|
|
2785
3850
|
const payload = { ...claimed.payload };
|
|
@@ -2861,6 +3926,85 @@ export class FleetManager {
|
|
|
2861
3926
|
}
|
|
2862
3927
|
await laneReleased;
|
|
2863
3928
|
}
|
|
3929
|
+
/** The ordinary durable lane, with provider completion proof instead of terminal/IPC submission proof. */
|
|
3930
|
+
async dispatchNativeDurableDelivery(claimed) {
|
|
3931
|
+
const outbox = this.deliveryOutbox;
|
|
3932
|
+
if (!outbox)
|
|
3933
|
+
return;
|
|
3934
|
+
const { deliveryId, targetDaemonBootId, attemptNo, targetInstance: target } = claimed;
|
|
3935
|
+
let begun = false;
|
|
3936
|
+
try {
|
|
3937
|
+
if (["raw_paste", "steer"].includes(claimed.kind)) {
|
|
3938
|
+
if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) === "begun")
|
|
3939
|
+
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "failed", "Windows runtime does not support terminal input or steering");
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
const meta = claimed.payload.meta && typeof claimed.payload.meta === "object" ? claimed.payload.meta : {};
|
|
3943
|
+
const task = await this.nativeInstanceRuntime().execute(target, { ...claimed.payload, requestId: deliveryId, meta: { ...meta, from_instance: claimed.sourceInstance, correlation_id: claimed.correlationId } }, {
|
|
3944
|
+
stillCurrent: () => !this.shuttingDown,
|
|
3945
|
+
beforeStart: () => { if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) !== "begun")
|
|
3946
|
+
throw new Error("Windows delivery attempt is no longer current"); begun = true; },
|
|
3947
|
+
});
|
|
3948
|
+
const outcome = task.outcomeConfirmed === false ? "uncertain" : task.status === "completed" ? "delivered" : "failed";
|
|
3949
|
+
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, outcome, task.error || task.status);
|
|
3950
|
+
// Return a requested peer report when the provider did not explicitly report it through scoped MCP.
|
|
3951
|
+
if (meta.requires_reply === "true" && claimed.correlationId && !task.reportedCorrelationIds?.includes(claimed.correlationId) && this.fleetConfig?.instances[claimed.sourceInstance]) {
|
|
3952
|
+
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
3953
|
+
outbox.admit({ sourceKey: `native-result:${deliveryId}`, operationId: `native-result:${deliveryId}`, sourceInstance: target, sourceDaemonBootId: this.managerBootId,
|
|
3954
|
+
targetInstance: claimed.sourceInstance, kind: "fleet_inbound", correlationId: claimed.correlationId,
|
|
3955
|
+
payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: target, correlation_id: claimed.correlationId, requires_reply: "false", source: "native_runtime" } } });
|
|
3956
|
+
this.scheduleDeliveryOutboxPump();
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
catch (error) {
|
|
3960
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3961
|
+
if (begun)
|
|
3962
|
+
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "uncertain", reason);
|
|
3963
|
+
else
|
|
3964
|
+
outbox.retryBeforeBegin(deliveryId, targetDaemonBootId, attemptNo, reason, 5000);
|
|
3965
|
+
this.logger.warn({ target, deliveryId, err: reason }, "Windows durable delivery did not complete");
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
/** A runtime's persisted completion can outlive this manager. Resolve its original delivery fence, never send
|
|
3969
|
+
* the prompt again, and reconstruct a missing peer report using the same durable operation ID. */
|
|
3970
|
+
reconcileRemoteDelivery(machineId, task) {
|
|
3971
|
+
const outbox = this.deliveryOutbox;
|
|
3972
|
+
if (!outbox?.isOpen || !task.finishedAt || task.outcomeConfirmed !== true || !["completed", "failed", "cancelled"].includes(task.status))
|
|
3973
|
+
return;
|
|
3974
|
+
const key = `${task.id}:${task.finishedAt}:${task.status}`;
|
|
3975
|
+
if (this.reconciledRemoteResults.has(key))
|
|
3976
|
+
return;
|
|
3977
|
+
const row = outbox.get(task.id);
|
|
3978
|
+
if (!row || !isNativeExecInstance(this.fleetConfig?.instances[row.targetInstance]) || this.fleetConfig?.instances[row.targetInstance]?.machine_id !== machineId
|
|
3979
|
+
|| !row.targetDaemonBootId || this.activeDurableTargets.has(row.targetInstance))
|
|
3980
|
+
return;
|
|
3981
|
+
if (row.state === "submission_started" || row.state === "reconciliation_pending") {
|
|
3982
|
+
if (!outbox.complete(row.deliveryId, row.targetDaemonBootId, row.attemptNo, task.status === "completed" ? "delivered" : "failed", task.error || "Runtime completion recovered after reconnect"))
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
else if (row.state === "uncertain" && task.status === "completed") {
|
|
3986
|
+
if (outbox.markConsumed(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "turn", "Paired runtime journal confirmed this exact execution completed") === "ignored")
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
else if (row.state !== "delivered" && row.state !== "failed")
|
|
3990
|
+
return;
|
|
3991
|
+
const meta = row.payload.meta && typeof row.payload.meta === "object" ? row.payload.meta : {};
|
|
3992
|
+
if (meta.requires_reply !== "true" || !row.correlationId || !this.fleetConfig?.instances[row.sourceInstance]) {
|
|
3993
|
+
this.reconciledRemoteResults.add(key);
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
const reports = outbox.queryStatusForInstance(row.targetInstance, { correlationId: row.correlationId, limit: 100 }).items;
|
|
3997
|
+
if (reports.some(report => report.source_instance === row.targetInstance && report.target_instance === row.sourceInstance
|
|
3998
|
+
&& report.kind === "report" && !["failed", "cancelled"].includes(report.state))) {
|
|
3999
|
+
this.reconciledRemoteResults.add(key);
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
4003
|
+
outbox.admit({ sourceKey: `native-result:${row.deliveryId}`, operationId: `native-result:${row.deliveryId}`, sourceInstance: row.targetInstance, sourceDaemonBootId: this.managerBootId,
|
|
4004
|
+
targetInstance: row.sourceInstance, kind: "fleet_inbound", correlationId: row.correlationId,
|
|
4005
|
+
payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: row.targetInstance, correlation_id: row.correlationId, requires_reply: "false", source: "native_runtime" } } });
|
|
4006
|
+
this.reconciledRemoteResults.add(key);
|
|
4007
|
+
}
|
|
2864
4008
|
async waitForDurableLaneRelease(claimed) {
|
|
2865
4009
|
const outbox = this.deliveryOutbox;
|
|
2866
4010
|
if (!outbox)
|
|
@@ -3317,6 +4461,7 @@ export class FleetManager {
|
|
|
3317
4461
|
this.warnAboutRemovedLoginMode(loaded);
|
|
3318
4462
|
this.enforceGuestSwitch();
|
|
3319
4463
|
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
4464
|
+
this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
|
|
3320
4465
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
3321
4466
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
3322
4467
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -3745,6 +4890,8 @@ export class FleetManager {
|
|
|
3745
4890
|
}
|
|
3746
4891
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
3747
4892
|
getAdapterForInstance(name) {
|
|
4893
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
4894
|
+
return null;
|
|
3748
4895
|
const worldId = this.getInstanceAdapterId(name);
|
|
3749
4896
|
if (worldId)
|
|
3750
4897
|
return this.worlds.get(worldId)?.adapter ?? this.adapter;
|
|
@@ -3807,6 +4954,8 @@ export class FleetManager {
|
|
|
3807
4954
|
}
|
|
3808
4955
|
/** Get the group_id for an instance's bound adapter */
|
|
3809
4956
|
getGroupIdForInstance(name) {
|
|
4957
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
4958
|
+
return "";
|
|
3810
4959
|
const adapterId = this.getInstanceAdapterId(name);
|
|
3811
4960
|
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
3812
4961
|
return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
@@ -3843,6 +4992,8 @@ export class FleetManager {
|
|
|
3843
4992
|
*/
|
|
3844
4993
|
getInstanceAdapterId(name) {
|
|
3845
4994
|
const cfg = this.fleetConfig?.instances[name];
|
|
4995
|
+
if (isNativeExecInstance(cfg))
|
|
4996
|
+
return undefined;
|
|
3846
4997
|
if (cfg)
|
|
3847
4998
|
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
3848
4999
|
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
|
|
@@ -3870,9 +5021,32 @@ export class FleetManager {
|
|
|
3870
5021
|
this.instanceWorldBinding.set(name, adapterId);
|
|
3871
5022
|
}
|
|
3872
5023
|
getInstanceStatus(name) {
|
|
5024
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5025
|
+
try {
|
|
5026
|
+
return this.nativeInstanceRuntime().status(name);
|
|
5027
|
+
}
|
|
5028
|
+
catch {
|
|
5029
|
+
return "crashed";
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
3873
5032
|
if (this.lifecycle.isPaused(name))
|
|
3874
5033
|
return "paused";
|
|
3875
5034
|
const daemon = this.lifecycle.daemons.get(name);
|
|
5035
|
+
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
5036
|
+
const config = this.fleetConfig.instances[name];
|
|
5037
|
+
if (config.machine_id ? !remoteRuntimeEnabled() : !localWindowsCliEnabled())
|
|
5038
|
+
return "stopped";
|
|
5039
|
+
if (daemon) {
|
|
5040
|
+
const native = daemon, status = native.getProcessStatus?.() ?? "crashed";
|
|
5041
|
+
return status === "running" && native.nativePaneCurrent?.() !== true ? "crashed" : status;
|
|
5042
|
+
}
|
|
5043
|
+
try {
|
|
5044
|
+
return this.nativeInstanceRuntime().get(name)?.activeId ? "crashed" : "stopped";
|
|
5045
|
+
}
|
|
5046
|
+
catch {
|
|
5047
|
+
return "crashed";
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
3876
5050
|
const processStatus = this.instanceProcessStatus.get(name);
|
|
3877
5051
|
// IPC can be disconnected during a respawn, so the event which announces
|
|
3878
5052
|
// the new live pane may be missed. The in-process daemon is authoritative
|
|
@@ -3906,6 +5080,14 @@ export class FleetManager {
|
|
|
3906
5080
|
}
|
|
3907
5081
|
}
|
|
3908
5082
|
getInstanceExecutionState(name) {
|
|
5083
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5084
|
+
try {
|
|
5085
|
+
return this.nativeInstanceRuntime().execution(name);
|
|
5086
|
+
}
|
|
5087
|
+
catch {
|
|
5088
|
+
return null;
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
3909
5091
|
if (this.lifecycle.isPaused(name))
|
|
3910
5092
|
return null;
|
|
3911
5093
|
// Process status wins over a stale pane snapshot. A dead remain-on-exit pane
|
|
@@ -3916,6 +5098,8 @@ export class FleetManager {
|
|
|
3916
5098
|
}
|
|
3917
5099
|
/** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
|
|
3918
5100
|
getInstanceInteraction(name) {
|
|
5101
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5102
|
+
return null;
|
|
3919
5103
|
if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
|
|
3920
5104
|
return null;
|
|
3921
5105
|
const daemon = this.lifecycle.daemons.get(name);
|
|
@@ -4003,6 +5187,8 @@ export class FleetManager {
|
|
|
4003
5187
|
bindings.push([name, backend, profile]);
|
|
4004
5188
|
};
|
|
4005
5189
|
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
5190
|
+
if (isWindowsInstance(config))
|
|
5191
|
+
continue; // Windows sign-ins and usage do not belong to the Linux account.
|
|
4006
5192
|
// loadFleetConfig() has already merged the fleet default into each row.
|
|
4007
5193
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
4008
5194
|
add(name, backend, instanceCredentialProfile(config, this.fleetConfig?.defaults, backend));
|
|
@@ -4609,6 +5795,8 @@ export class FleetManager {
|
|
|
4609
5795
|
* id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
|
|
4610
5796
|
*/
|
|
4611
5797
|
startWebChatProgress(instanceName) {
|
|
5798
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
5799
|
+
return Promise.resolve();
|
|
4612
5800
|
if (this.shuttingDown)
|
|
4613
5801
|
return Promise.resolve();
|
|
4614
5802
|
return this.sendCancelButton(instanceName);
|
|
@@ -4874,6 +6062,12 @@ export class FleetManager {
|
|
|
4874
6062
|
}
|
|
4875
6063
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
4876
6064
|
async deliverToInstance(instanceName, payload, options = {}) {
|
|
6065
|
+
if (this.generalLocationDeliveryHold(instanceName))
|
|
6066
|
+
return false;
|
|
6067
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
6068
|
+
return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
|
|
6069
|
+
...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
|
|
6070
|
+
}
|
|
4877
6071
|
const deliveryEpoch = this.getDeliveryEpoch(instanceName);
|
|
4878
6072
|
const deliveryPayload = { ...payload, delivery_epoch: deliveryEpoch };
|
|
4879
6073
|
const meta = payload.meta && typeof payload.meta === "object"
|
|
@@ -5431,6 +6625,10 @@ export class FleetManager {
|
|
|
5431
6625
|
* slot admission and joins any wake already in flight for the target.
|
|
5432
6626
|
*/
|
|
5433
6627
|
async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
|
|
6628
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
6629
|
+
this.nativeInstanceRuntime().setPaused(name, false);
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
5434
6632
|
if (this.sessionOomStopped(name)) {
|
|
5435
6633
|
if (!retryStoppedSession)
|
|
5436
6634
|
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
@@ -5455,6 +6653,10 @@ export class FleetManager {
|
|
|
5455
6653
|
.sort((a, b) => (readLastInboundAt(this.getInstanceDir(a)) ?? 0) - (readLastInboundAt(this.getInstanceDir(b)) ?? 0))[0];
|
|
5456
6654
|
}
|
|
5457
6655
|
async changeInstancePauseState(name, action) {
|
|
6656
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
6657
|
+
this.nativeInstanceRuntime().setPaused(name, action === "pause");
|
|
6658
|
+
return action === "pause" ? "paused" : "awake";
|
|
6659
|
+
}
|
|
5458
6660
|
if (action === "wake") {
|
|
5459
6661
|
await this.explicitWake(name, 30_000, true);
|
|
5460
6662
|
this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
|
|
@@ -5632,6 +6834,14 @@ export class FleetManager {
|
|
|
5632
6834
|
resumePaused = false,
|
|
5633
6835
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
5634
6836
|
transition, execution, kiroMigration) {
|
|
6837
|
+
if (isNativeExecInstance(config)) {
|
|
6838
|
+
execution?.assert();
|
|
6839
|
+
if (!windowsCliEnabled())
|
|
6840
|
+
throw new Error("Windows runtime is disabled");
|
|
6841
|
+
if (resumePaused)
|
|
6842
|
+
this.nativeInstanceRuntime().setPaused(name, false);
|
|
6843
|
+
return;
|
|
6844
|
+
}
|
|
5635
6845
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
5636
6846
|
if (migration && migration.token !== kiroMigration)
|
|
5637
6847
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -5682,27 +6892,29 @@ export class FleetManager {
|
|
|
5682
6892
|
if (resumePaused)
|
|
5683
6893
|
this.lifecycle.markActivitySeedNow(name);
|
|
5684
6894
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5685
|
-
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
6895
|
+
// #1490 row 67 (main): off-loop resolve before skill/instruction publication
|
|
5686
6896
|
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
5687
6897
|
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
5688
6898
|
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
5689
6899
|
// accept the stale start. Lifecycle start looks up again before its own
|
|
5690
|
-
// createBackend; the probe cache makes that cheap.
|
|
6900
|
+
// createBackend; the probe cache makes that cheap. (alpha: a native Windows
|
|
6901
|
+
// agent's CLI is on that PC — nothing to resolve on this one.)
|
|
5691
6902
|
const epoch = this.lifecycle.epochOf(name);
|
|
5692
6903
|
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
5693
|
-
const
|
|
6904
|
+
const windows = isWindowsInstance(config);
|
|
6905
|
+
const resolved = windows ? null : await resolveBackendBinary(backend);
|
|
5694
6906
|
execution?.assert();
|
|
5695
6907
|
if (this.lifecycle.epochOf(name) !== epoch
|
|
5696
6908
|
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
5697
6909
|
throw new SupersededStartError(name);
|
|
5698
6910
|
}
|
|
5699
6911
|
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
5700
|
-
if (binary)
|
|
6912
|
+
if (binary && !windows)
|
|
5701
6913
|
noteResolvedBinary(binary, resolved ?? binary);
|
|
5702
|
-
if (config.general_topic) {
|
|
6914
|
+
if (!windows && config.general_topic) {
|
|
5703
6915
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
5704
6916
|
}
|
|
5705
|
-
else if (kind === "fleet-topic") {
|
|
6917
|
+
else if (!isWindowsInstance(config) && kind === "fleet-topic") {
|
|
5706
6918
|
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
5707
6919
|
// deliberately excluded: their workspace and conversation lifecycle are
|
|
5708
6920
|
// managed independently from fleet-topic workers.
|
|
@@ -5717,7 +6929,7 @@ export class FleetManager {
|
|
|
5717
6929
|
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
5718
6930
|
}
|
|
5719
6931
|
}
|
|
5720
|
-
else if (kind === "classic") {
|
|
6932
|
+
else if (!isWindowsInstance(config) && kind === "classic") {
|
|
5721
6933
|
// #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
|
|
5722
6934
|
// coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
|
|
5723
6935
|
// room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
|
|
@@ -5734,7 +6946,7 @@ export class FleetManager {
|
|
|
5734
6946
|
}
|
|
5735
6947
|
}
|
|
5736
6948
|
const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
|
|
5737
|
-
const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
|
|
6949
|
+
const identity = { kind, backend, model: isWindowsInstance(config) ? config.model || "default" : this.resolveInstanceModel(name).display };
|
|
5738
6950
|
if (receipt) {
|
|
5739
6951
|
await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
|
|
5740
6952
|
// Only this transition's actual publication can acquire its replacement.
|
|
@@ -5804,7 +7016,7 @@ export class FleetManager {
|
|
|
5804
7016
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
5805
7017
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
5806
7018
|
// full stagger slot for every distinct working directory.
|
|
5807
|
-
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
7019
|
+
const runnableEntries = entries.filter(([name, config]) => !isNativeExecInstance(config) && !this.lifecycle.isPaused(name));
|
|
5808
7020
|
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
5809
7021
|
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
5810
7022
|
const fleetNames = new Set(entries.map(([name]) => name));
|
|
@@ -5870,6 +7082,8 @@ export class FleetManager {
|
|
|
5870
7082
|
return false;
|
|
5871
7083
|
if (err instanceof SessionOomStoppedError)
|
|
5872
7084
|
await this.notifySessionOomStop(name);
|
|
7085
|
+
if (this.holdNativePaneRecovery(name, err))
|
|
7086
|
+
return false;
|
|
5873
7087
|
if (this.stopOnUnsupportedCli(name, err, what))
|
|
5874
7088
|
return false;
|
|
5875
7089
|
this.logger.error({ err, name }, `Failed to start ${what}`);
|
|
@@ -6011,6 +7225,8 @@ export class FleetManager {
|
|
|
6011
7225
|
if (err instanceof SupersededStartError)
|
|
6012
7226
|
return;
|
|
6013
7227
|
// An earlier attempt could not tell (unknown probe); this one could.
|
|
7228
|
+
if (this.holdNativePaneRecovery(name, err))
|
|
7229
|
+
return;
|
|
6014
7230
|
if (this.stopOnUnsupportedCli(name, err, "instance"))
|
|
6015
7231
|
return;
|
|
6016
7232
|
this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
|
|
@@ -6261,7 +7477,7 @@ export class FleetManager {
|
|
|
6261
7477
|
return count;
|
|
6262
7478
|
}
|
|
6263
7479
|
configuredStartupInstanceNames(fleet, includeClassic) {
|
|
6264
|
-
const names = new Set(Object.
|
|
7480
|
+
const names = new Set(Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config)).map(([name]) => name));
|
|
6265
7481
|
if (includeClassic) {
|
|
6266
7482
|
for (const channel of this.classicChannels?.getAll() ?? [])
|
|
6267
7483
|
names.add(channel.instanceName);
|
|
@@ -6312,6 +7528,10 @@ export class FleetManager {
|
|
|
6312
7528
|
return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
|
|
6313
7529
|
}
|
|
6314
7530
|
async stopInstance(name, transition) {
|
|
7531
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7532
|
+
this.nativeInstanceRuntime().setPaused(name, true);
|
|
7533
|
+
return { backgroundWorkEnded: false };
|
|
7534
|
+
}
|
|
6315
7535
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
6316
7536
|
this.cancelStartupRetry(name);
|
|
6317
7537
|
this.failoverActive.delete(name);
|
|
@@ -6351,6 +7571,16 @@ export class FleetManager {
|
|
|
6351
7571
|
async restartSingleInstance(name, opts) {
|
|
6352
7572
|
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
6353
7573
|
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
7574
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7575
|
+
const runtime = this.nativeInstanceRuntime();
|
|
7576
|
+
if (runtime.get(name)?.blocked)
|
|
7577
|
+
throw new Error("Verify the disconnected Windows process stopped and recover this agent before restarting");
|
|
7578
|
+
runtime.cancel(name);
|
|
7579
|
+
runtime.setPaused(name, false);
|
|
7580
|
+
return;
|
|
7581
|
+
}
|
|
7582
|
+
if (opts?.explicit && isNativePaneInstance(this.fleetConfig?.instances[name]))
|
|
7583
|
+
this.lifecycle.resetNativeCrashBudget(name);
|
|
6354
7584
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
6355
7585
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
6356
7586
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -6414,7 +7644,7 @@ export class FleetManager {
|
|
|
6414
7644
|
return run;
|
|
6415
7645
|
}
|
|
6416
7646
|
isInstanceRestarting(name) {
|
|
6417
|
-
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
7647
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true || this.lifecycle?.nativeRecoveryPending?.(name) === true;
|
|
6418
7648
|
}
|
|
6419
7649
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
6420
7650
|
restartInFlight(name) {
|
|
@@ -6709,6 +7939,9 @@ export class FleetManager {
|
|
|
6709
7939
|
log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
|
|
6710
7940
|
}
|
|
6711
7941
|
publicOwnerCurrent(owner) {
|
|
7942
|
+
// #3008: the outside link is this computer's admin's — while "Allow connections from outside" is on, web-only.
|
|
7943
|
+
if (isOutsideLinkOwner(owner))
|
|
7944
|
+
return !this.shuttingDown && this.outsideOn();
|
|
6712
7945
|
if (this.shuttingDown || !this.hasFleetAdmins(owner.adapterId) || !this.isFleetAdmin(owner.userId, owner.adapterId))
|
|
6713
7946
|
return false;
|
|
6714
7947
|
const configured = (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).find(ch => (ch.id ?? ch.type) === owner.adapterId);
|
|
@@ -6798,7 +8031,11 @@ export class FleetManager {
|
|
|
6798
8031
|
}
|
|
6799
8032
|
getPublicWebLink() {
|
|
6800
8033
|
this.initializeWebSessions();
|
|
6801
|
-
return this.publicWebLink ??= new PublicWebLink(
|
|
8034
|
+
return this.publicWebLink ??= new PublicWebLink(this.publicWebLinkDeps());
|
|
8035
|
+
}
|
|
8036
|
+
/** The public link's wiring to this fleet (a test builds one with the tunnel itself stubbed: `ensure`, `provider`). */
|
|
8037
|
+
publicWebLinkDeps() {
|
|
8038
|
+
return {
|
|
6802
8039
|
dataDir: this.dataDir, web: () => this.fleetConfig?.web,
|
|
6803
8040
|
permitted: owner => this.publicOwnerCurrent(owner),
|
|
6804
8041
|
reserve: id => this.getTunnelLane().reserve("dashboard", id),
|
|
@@ -6831,7 +8068,9 @@ export class FleetManager {
|
|
|
6831
8068
|
if (world && world.adapter === owner.binding)
|
|
6832
8069
|
void world.adapter.sendText(owner.chatId, t("dashboard.public_cleanup"), { threadId: owner.threadId }).catch(() => { });
|
|
6833
8070
|
},
|
|
6834
|
-
|
|
8071
|
+
onClose: (owner, reason) => { if (isOutsideLinkOwner(owner))
|
|
8072
|
+
this.outsideLinkClosed(reason); },
|
|
8073
|
+
};
|
|
6835
8074
|
}
|
|
6836
8075
|
publicLinkAutoOpen = null;
|
|
6837
8076
|
/**
|
|
@@ -8190,6 +9429,9 @@ export class FleetManager {
|
|
|
8190
9429
|
const fleet = this.loadConfig(configPath, { startup: true });
|
|
8191
9430
|
this.slimFleetConfigAtStartup();
|
|
8192
9431
|
this.logConfigWarningsAtStartup();
|
|
9432
|
+
this.initializeNativeInstances();
|
|
9433
|
+
// M-2255: from here on a change to the Windows runtimes' switches applies live (observeRuntimeSwitches).
|
|
9434
|
+
this.runtimeEffects = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
8193
9435
|
setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
|
|
8194
9436
|
// #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
|
|
8195
9437
|
const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
|
|
@@ -8221,7 +9463,7 @@ export class FleetManager {
|
|
|
8221
9463
|
// Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
|
|
8222
9464
|
// before instances spawn, so a CLI that comes up on its sign-in screen gets
|
|
8223
9465
|
// an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
|
|
8224
|
-
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).map(config => {
|
|
9466
|
+
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).filter(config => !isWindowsInstance(config)).map(config => {
|
|
8225
9467
|
const backend = config.backend ?? fleet.defaults?.backend ?? "claude-code";
|
|
8226
9468
|
// #1699 follow-up: each instance's own login — a profile is probed with its own environment.
|
|
8227
9469
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, fleet.defaults, backend) : null };
|
|
@@ -8283,6 +9525,7 @@ export class FleetManager {
|
|
|
8283
9525
|
// #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
|
|
8284
9526
|
// rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
|
|
8285
9527
|
this.restoreWebChat(fleet);
|
|
9528
|
+
this.initializeNativeInstances(true);
|
|
8286
9529
|
this.startSearchIndex(fleet);
|
|
8287
9530
|
// Parse-only source monitoring; it does not apply hand-edited Fleet settings.
|
|
8288
9531
|
this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
|
|
@@ -8465,7 +9708,7 @@ export class FleetManager {
|
|
|
8465
9708
|
}
|
|
8466
9709
|
}
|
|
8467
9710
|
// Phase 1: Start general instances first and wait for them
|
|
8468
|
-
const allEntries = Object.entries(fleet.instances);
|
|
9711
|
+
const allEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
|
|
8469
9712
|
const generals = allEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
8470
9713
|
const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
8471
9714
|
const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
|
|
@@ -8704,6 +9947,10 @@ export class FleetManager {
|
|
|
8704
9947
|
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
8705
9948
|
this.startPublicLinkAutoOpen();
|
|
8706
9949
|
}
|
|
9950
|
+
// #3008: connections from outside were on when the fleet stopped: the link opens again (its address is new for a
|
|
9951
|
+
// quick link — Settings shows it on this computer).
|
|
9952
|
+
if (this.outsideOn())
|
|
9953
|
+
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
8707
9954
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
8708
9955
|
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
8709
9956
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
@@ -9467,10 +10714,14 @@ export class FleetManager {
|
|
|
9467
10714
|
}
|
|
9468
10715
|
}
|
|
9469
10716
|
}
|
|
10717
|
+
const daemon = this.daemons.get(name);
|
|
10718
|
+
const native = daemon?.usesNativePane?.();
|
|
9470
10719
|
const sockPath = join(this.getInstanceDir(name), "channel.sock");
|
|
9471
|
-
if (!existsSync(sockPath))
|
|
10720
|
+
if (!native && !existsSync(sockPath))
|
|
9472
10721
|
return;
|
|
9473
|
-
const ipc =
|
|
10722
|
+
const ipc = native
|
|
10723
|
+
? new NativeDaemonClient(daemon, () => this.daemons.get(name) === daemon && (!this.instanceIpcClients.has(name) || this.instanceIpcClients.get(name) === ipc))
|
|
10724
|
+
: new IpcClient(sockPath);
|
|
9474
10725
|
try {
|
|
9475
10726
|
await ipc.connect();
|
|
9476
10727
|
this.instanceIpcClients.set(name, ipc);
|
|
@@ -9586,7 +10837,7 @@ export class FleetManager {
|
|
|
9586
10837
|
// connected, so seed the cache instead of waiting for another transition.
|
|
9587
10838
|
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
9588
10839
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
9589
|
-
if (!this.statuslineWatcher.has(name)) {
|
|
10840
|
+
if (!native && !this.statuslineWatcher.has(name)) {
|
|
9590
10841
|
this.statuslineWatcher.watch(name);
|
|
9591
10842
|
}
|
|
9592
10843
|
// Auto-reconnect on disconnect (unless intentionally stopping)
|
|
@@ -10633,6 +11884,12 @@ export class FleetManager {
|
|
|
10633
11884
|
if (msg.adapterId)
|
|
10634
11885
|
this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
|
|
10635
11886
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
11887
|
+
const locationHold = this.generalLocationDeliveryHold(generalInstance);
|
|
11888
|
+
if (locationHold) {
|
|
11889
|
+
await inboundAdapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
11890
|
+
this.needsYou?.poke();
|
|
11891
|
+
return;
|
|
11892
|
+
}
|
|
10636
11893
|
// React immediately — before any other API calls — through the status
|
|
10637
11894
|
// path, which tracks Telegram slot ownership and Discord status adds.
|
|
10638
11895
|
if (msg.chatId && msg.messageId) {
|
|
@@ -10720,6 +11977,13 @@ export class FleetManager {
|
|
|
10720
11977
|
if (instanceConfig?.general_topic && await this.topicCommands.handleGeneralCommand(msg, instanceName)) {
|
|
10721
11978
|
return;
|
|
10722
11979
|
}
|
|
11980
|
+
const locationHold = this.generalLocationDeliveryHold(instanceName);
|
|
11981
|
+
if (locationHold) {
|
|
11982
|
+
const adapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
11983
|
+
await adapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
11984
|
+
this.needsYou?.poke();
|
|
11985
|
+
return;
|
|
11986
|
+
}
|
|
10723
11987
|
// Bind instance to the adapter that delivered this message
|
|
10724
11988
|
if (msg.adapterId)
|
|
10725
11989
|
this.bindInstanceAdapter(instanceName, msg.adapterId, true);
|
|
@@ -12649,6 +13913,7 @@ export class FleetManager {
|
|
|
12649
13913
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12650
13914
|
this.refreshSavedDashboardPermissions();
|
|
12651
13915
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
13916
|
+
this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
|
|
12652
13917
|
}
|
|
12653
13918
|
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
12654
13919
|
refreshSavedDashboardPermissions() {
|
|
@@ -12882,10 +14147,43 @@ export class FleetManager {
|
|
|
12882
14147
|
}
|
|
12883
14148
|
getSettingsConfigPath() { return this.configPath; }
|
|
12884
14149
|
async removeInstance(name, authorization, execution) {
|
|
14150
|
+
if (isWindowsInstance(this.fleetConfig?.instances[name])) {
|
|
14151
|
+
execution?.assert();
|
|
14152
|
+
assertExplicitInstanceRemoval(authorization);
|
|
14153
|
+
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
14154
|
+
await this.stopInstance(name);
|
|
14155
|
+
execution?.assert();
|
|
14156
|
+
}
|
|
14157
|
+
this.nativeInstanceRuntime().remove(name, () => !this.shuttingDown && (!execution || execution.current()), execution);
|
|
14158
|
+
return;
|
|
14159
|
+
}
|
|
12885
14160
|
if (execution)
|
|
12886
14161
|
return this.lifecycle.runTransition(name, transition => this.removeInstanceOwned(name, authorization, execution, transition));
|
|
12887
14162
|
return this.removeInstanceOwned(name, authorization);
|
|
12888
14163
|
}
|
|
14164
|
+
/** Shared instance metadata goes with a native identity; its Windows workspace and provider sessions stay intact. */
|
|
14165
|
+
nativeInstanceRemoved(name) {
|
|
14166
|
+
const clean = (what, run) => { try {
|
|
14167
|
+
run();
|
|
14168
|
+
}
|
|
14169
|
+
catch (error) {
|
|
14170
|
+
this.logger.warn({ name, err: error }, `Native instance ${what} cleanup failed`);
|
|
14171
|
+
} };
|
|
14172
|
+
clean("delivery", () => { this.cancelPendingDeliveries(name); });
|
|
14173
|
+
forgetInstanceContext(name);
|
|
14174
|
+
clean("schedules", () => { for (const schedule of this.scheduler?.list(name) ?? [])
|
|
14175
|
+
this.scheduler.delete(schedule.id); });
|
|
14176
|
+
this.lastActivity.delete(name);
|
|
14177
|
+
this.lastInboundUser.delete(name);
|
|
14178
|
+
this.rateLimitWarnedAt.delete(name);
|
|
14179
|
+
this.lastInboundMsg.delete(name);
|
|
14180
|
+
clean("history", () => this.webChatHistory.forget(name));
|
|
14181
|
+
clean("search", () => this.searchIndex?.removeInstance(name));
|
|
14182
|
+
clean("files", () => this.webFiles.forget(name));
|
|
14183
|
+
clean("compact", () => this.compactWatchService?.forget(name));
|
|
14184
|
+
clean("preferences", () => this.webPrefs.forgetInstance(name));
|
|
14185
|
+
clean("appearance", () => { this.themeStore.forget(name); this.webAppearance.forget(name); this.appearanceTools.forget(name); this.mods.forgetInstance(name); this.officeActsInstance?.forget(name); this.officePackToolsInstance?.forget(name); });
|
|
14186
|
+
}
|
|
12889
14187
|
async removeInstanceOwned(name, authorization, execution, transition) {
|
|
12890
14188
|
execution?.assert();
|
|
12891
14189
|
assertExplicitInstanceRemoval(authorization);
|
|
@@ -13011,6 +14309,8 @@ export class FleetManager {
|
|
|
13011
14309
|
execution?.complete();
|
|
13012
14310
|
}
|
|
13013
14311
|
startStatuslineWatcher(name) {
|
|
14312
|
+
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
14313
|
+
return;
|
|
13014
14314
|
if (this.lifecycle.isPaused(name))
|
|
13015
14315
|
return;
|
|
13016
14316
|
this.statuslineWatcher.watch(name);
|
|
@@ -14459,6 +15759,10 @@ export class FleetManager {
|
|
|
14459
15759
|
* would have to make this async and change every caller.
|
|
14460
15760
|
*/
|
|
14461
15761
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
15762
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
15763
|
+
this.emitSseEvent("message", { instance: instanceName, role: "status", sender: "AgEnD", text, ts: new Date().toISOString() });
|
|
15764
|
+
return true;
|
|
15765
|
+
}
|
|
14462
15766
|
// #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
|
|
14463
15767
|
if (this.webOnlyPromptPlace())
|
|
14464
15768
|
return this.recordWebNotice("instance", text, instanceName);
|
|
@@ -14939,6 +16243,8 @@ export class FleetManager {
|
|
|
14939
16243
|
* (also for a backend that has no profiles, where the option is ignored).
|
|
14940
16244
|
*/
|
|
14941
16245
|
credentialProfileOf(name, backend) {
|
|
16246
|
+
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
16247
|
+
return null;
|
|
14942
16248
|
if (!credentialHomeSpec(backend))
|
|
14943
16249
|
return null;
|
|
14944
16250
|
const cfg = this.fleetConfig?.instances?.[name];
|
|
@@ -14961,7 +16267,7 @@ export class FleetManager {
|
|
|
14961
16267
|
}
|
|
14962
16268
|
/** The instances the cache analysis reads: fleet.yaml's, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend). */
|
|
14963
16269
|
cacheInstances() {
|
|
14964
|
-
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
16270
|
+
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
|
|
14965
16271
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14966
16272
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
14967
16273
|
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
@@ -15140,6 +16446,20 @@ export class FleetManager {
|
|
|
15140
16446
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
15141
16447
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
15142
16448
|
usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
|
|
16449
|
+
nativeDeliveryHold: this.daemons.get(name)?.nativeDeliveryHold?.() ?? null,
|
|
16450
|
+
generalLocation: this.generalLocationAttention(name),
|
|
16451
|
+
paneRecovery: (() => {
|
|
16452
|
+
const config = this.fleetConfig?.instances[name];
|
|
16453
|
+
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
16454
|
+
return null;
|
|
16455
|
+
try {
|
|
16456
|
+
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
16457
|
+
return profile.paneRecovery ? { key: `${profile.paneRecovery.kind}:${profile.paneRecovery.launchId ?? "-"}`, detail: new NativePaneRecoveryError(profile.paneRecovery).message, since: profile.paneRecoveryAt ?? profile.createdAt } : null;
|
|
16458
|
+
}
|
|
16459
|
+
catch {
|
|
16460
|
+
return null;
|
|
16461
|
+
}
|
|
16462
|
+
})(),
|
|
15143
16463
|
});
|
|
15144
16464
|
}
|
|
15145
16465
|
return out;
|
|
@@ -15195,6 +16515,8 @@ export class FleetManager {
|
|
|
15195
16515
|
const target = this.fleetNoticeTarget(world);
|
|
15196
16516
|
return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
|
|
15197
16517
|
},
|
|
16518
|
+
// The name the web page shows for the instance (its View alias, else its own name), never the raw id alone.
|
|
16519
|
+
instanceName: instance => resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[instance], profile: profileIdentities(this.dataDir).get(instance) }).display_name || instance,
|
|
15198
16520
|
instanceTopic: instance => {
|
|
15199
16521
|
const topic = this.fleetConfig?.instances[instance]?.topic_id;
|
|
15200
16522
|
if (topic != null)
|
|
@@ -15739,7 +17061,7 @@ export class FleetManager {
|
|
|
15739
17061
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
15740
17062
|
authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
|
|
15741
17063
|
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
|
|
15742
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
17064
|
+
hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
|
|
15743
17065
|
});
|
|
15744
17066
|
}
|
|
15745
17067
|
sessionAuthorizations = null;
|
|
@@ -15799,6 +17121,264 @@ export class FleetManager {
|
|
|
15799
17121
|
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
15800
17122
|
this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
|
|
15801
17123
|
}
|
|
17124
|
+
// ── #3008: connections from outside (a web-only fleet; decision 28c2d3a3 rule 3, the user's simplified form) ──
|
|
17125
|
+
webRemoteAccessState = null;
|
|
17126
|
+
/** The authenticator, the outside switch and the wrong-code limiter (web-remote-access.json), read on first use. */
|
|
17127
|
+
webRemoteAccess() {
|
|
17128
|
+
if (!this.dataDir)
|
|
17129
|
+
return null;
|
|
17130
|
+
this.webRemoteAccessState ??= new WebRemoteAccess(join(this.dataDir, WEB_REMOTE_ACCESS_FILE), this.logger);
|
|
17131
|
+
return this.webRemoteAccessState;
|
|
17132
|
+
}
|
|
17133
|
+
/** "Allow connections from outside" is on (an app set up) and this is a web-only fleet. */
|
|
17134
|
+
outsideOn() {
|
|
17135
|
+
return this.isWebOnlyFleet() && (this.webRemoteAccess()?.outside ?? false);
|
|
17136
|
+
}
|
|
17137
|
+
/** The link connections from outside opened, while it is open and still theirs: its exposure. Else null. */
|
|
17138
|
+
outsideLink() {
|
|
17139
|
+
if (!this.outsideOn())
|
|
17140
|
+
return null;
|
|
17141
|
+
const open = this.publicWebLink?.openSignin();
|
|
17142
|
+
return open && isOutsideLinkOwner(open.owner) ? { exposureId: open.exposureId } : null;
|
|
17143
|
+
}
|
|
17144
|
+
/**
|
|
17145
|
+
* The request's live session, asked again each time `current()` is called (its own cookie authenticated anew, the same
|
|
17146
|
+
* handle): this web-only fleet's owner — this computer's admin, on this computer (`hostOnly`), or also signed in from
|
|
17147
|
+
* outside — and web-verified now. Null when it is not (a guest, a LAN session, a chat fleet's admin, nobody).
|
|
17148
|
+
*/
|
|
17149
|
+
liveOwner(req, opts) {
|
|
17150
|
+
if (!this.isWebOnlyFleet())
|
|
17151
|
+
return null;
|
|
17152
|
+
if (opts.hostOnly && gatewayRequestContext(req))
|
|
17153
|
+
return null;
|
|
17154
|
+
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
17155
|
+
if (auth.kind !== "ok")
|
|
17156
|
+
return null;
|
|
17157
|
+
const handle = auth.session.handle, peer = webPeerOf(req), gateway = !!gatewayRequestContext(req);
|
|
17158
|
+
const owner = () => {
|
|
17159
|
+
if (!this.isWebOnlyFleet())
|
|
17160
|
+
return false;
|
|
17161
|
+
const live = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
17162
|
+
if (live.kind !== "ok" || live.session.handle !== handle)
|
|
17163
|
+
return false;
|
|
17164
|
+
if (opts.hostOnly && (live.session.host !== true || peer !== "loopback"))
|
|
17165
|
+
return false;
|
|
17166
|
+
const now = this.webAuthority({ session: live.session, gateway, peer });
|
|
17167
|
+
const a = now.authority.kind === "web-verified" && now.authority.session?.handle === handle ? now.authority.actor : null;
|
|
17168
|
+
return !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId;
|
|
17169
|
+
};
|
|
17170
|
+
return owner() ? { label: t("settings_confirm.host_operator_label"), handle, current: owner } : null;
|
|
17171
|
+
}
|
|
17172
|
+
/** Every session signed in from outside ends (and the authorizations kept for them). */
|
|
17173
|
+
endOutsideSessions() {
|
|
17174
|
+
const ended = new Set(this.webSessions?.list(undefined, r => r.remote === "totp").map(r => r.handle) ?? []);
|
|
17175
|
+
if (!ended.size)
|
|
17176
|
+
return;
|
|
17177
|
+
this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
|
|
17178
|
+
const result = this.webSessions?.revokeAll(r => r.remote === "totp");
|
|
17179
|
+
if (result && !result.durable)
|
|
17180
|
+
this.logger.warn("Outside sessions ended, but that could not be saved");
|
|
17181
|
+
this.logger.info({ count: ended.size }, "Sessions signed in from outside ended");
|
|
17182
|
+
}
|
|
17183
|
+
/**
|
|
17184
|
+
* #1939 §10 / #3008: a stored receipt's authority as it reads now — the web authority state, and for this computer's
|
|
17185
|
+
* admin from outside the switch and the last revocation of remote authority (Prism P2 2).
|
|
17186
|
+
*/
|
|
17187
|
+
receiptAuthorityNow(stored, state = this.webAuthorityState()) {
|
|
17188
|
+
return authorityNow(stored, { ...state, outside: this.outsideOn(), outsideHandles: this.webRemoteAccess()?.admitted ?? new Set() });
|
|
17189
|
+
}
|
|
17190
|
+
/** #3008 (the sign-in page): this exposure is the outside link, open now. */
|
|
17191
|
+
isOutsideLink(exposureId) {
|
|
17192
|
+
return this.outsideLink()?.exposureId === exposureId;
|
|
17193
|
+
}
|
|
17194
|
+
/** What Settings shows: set up or not, the switch, a wait in force, the link — its address only to this computer. */
|
|
17195
|
+
webOutsideStatus(req) {
|
|
17196
|
+
const store = this.webRemoteAccess(), status = store?.status();
|
|
17197
|
+
const here = !!this.liveOwner(req, { hostOnly: true });
|
|
17198
|
+
const open = this.outsideOn() ? this.publicWebLink?.openSignin() : null;
|
|
17199
|
+
const mine = !!open && isOutsideLinkOwner(open.owner);
|
|
17200
|
+
const linkStatus = this.outsideOn() ? this.getPublicWebStatus() : null;
|
|
17201
|
+
return { webOnly: this.isWebOnlyFleet(), here, enrolled: !!status?.enrolled, enrolledAt: status?.enrolledAt ?? null,
|
|
17202
|
+
outside: this.outsideOn(), pausedForMs: status?.pausedForMs ?? 0,
|
|
17203
|
+
link: linkStatus && linkStatus.state !== "closed" ? { ...linkStatus, ...(here && mine ? { url: open.url } : {}), quick: !publicLinkNamedHostname(this.fleetConfig?.web) } : null };
|
|
17204
|
+
}
|
|
17205
|
+
/** Set up (or set up again): a new secret, shown once to this computer's admin. Null: not them. */
|
|
17206
|
+
webOutsideBeginEnrollment(req) {
|
|
17207
|
+
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17208
|
+
if (!owner || !store)
|
|
17209
|
+
return null;
|
|
17210
|
+
return store.beginEnrollment(owner.handle, hostname() || "fleet");
|
|
17211
|
+
}
|
|
17212
|
+
/** The new app's first code: on "ok" it replaces the old one, whose sessions from outside end. */
|
|
17213
|
+
webOutsideConfirmEnrollment(req, code) {
|
|
17214
|
+
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17215
|
+
if (!owner || !store || !owner.current())
|
|
17216
|
+
return "not_here";
|
|
17217
|
+
const had = store.enrolled;
|
|
17218
|
+
const r = store.confirmEnrollment(owner.handle, code);
|
|
17219
|
+
if (r === "ok") {
|
|
17220
|
+
this.logger.info({ again: had }, "Authenticator app set up for connections from outside");
|
|
17221
|
+
if (had)
|
|
17222
|
+
this.endOutsideSessions();
|
|
17223
|
+
}
|
|
17224
|
+
return r;
|
|
17225
|
+
}
|
|
17226
|
+
/** Forget the app: the switch goes off, the link closes, every session from outside ends. */
|
|
17227
|
+
webOutsideRemove(req) {
|
|
17228
|
+
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17229
|
+
if (!owner || !store || !owner.current())
|
|
17230
|
+
return "not_here";
|
|
17231
|
+
// #3008 review (Prism P2 1): shut at once whatever the disk says; say so when it could not be saved (retried).
|
|
17232
|
+
const saved = store.remove();
|
|
17233
|
+
this.closeOutsideLink("outside removed");
|
|
17234
|
+
this.logger.info({ saved }, "Authenticator app removed; connections from outside off");
|
|
17235
|
+
return saved ? "ok" : "unsaved";
|
|
17236
|
+
}
|
|
17237
|
+
/** The switch. On: needs an app set up; the link opens. Off: it closes and every session from outside ends. */
|
|
17238
|
+
webOutsideSwitch(req, on) {
|
|
17239
|
+
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17240
|
+
if (!owner || !store || !owner.current())
|
|
17241
|
+
return "not_here";
|
|
17242
|
+
if (on && !publicLinkSettings(this.fleetConfig?.web).allowed)
|
|
17243
|
+
return "not_allowed";
|
|
17244
|
+
const r = store.setOutside(on);
|
|
17245
|
+
if (r === "not_enrolled")
|
|
17246
|
+
return r;
|
|
17247
|
+
this.logger.info({ on, saved: r === "ok" }, "Connections from outside switched");
|
|
17248
|
+
// #3008 review (Prism P2 1): Off shuts at once even when it could not be saved (the store retries); On opens only
|
|
17249
|
+
// once saved — a switch the disk does not hold would not survive a restart.
|
|
17250
|
+
if (!on)
|
|
17251
|
+
this.closeOutsideLink("outside off");
|
|
17252
|
+
else if (r === "ok")
|
|
17253
|
+
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
17254
|
+
return r;
|
|
17255
|
+
}
|
|
17256
|
+
outsideAttempt = null;
|
|
17257
|
+
outsideOpening = false;
|
|
17258
|
+
/**
|
|
17259
|
+
* Open the outside link: an attempt after each delay in turn until one opens (#3008 B: twice more at most — the quick
|
|
17260
|
+
* link's address service limits how often it may be asked). Every attempt asks again: the switch still on, the fleet
|
|
17261
|
+
* running. A final failure: one Notice; the switch stays on, so the next start tries again.
|
|
17262
|
+
*/
|
|
17263
|
+
openOutsideLink(delays) {
|
|
17264
|
+
this.outsideAttempt?.cancel();
|
|
17265
|
+
this.outsideAttempt = null;
|
|
17266
|
+
const attempt = (n) => {
|
|
17267
|
+
this.outsideAttempt = null;
|
|
17268
|
+
if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
|
|
17269
|
+
return;
|
|
17270
|
+
this.outsideOpening = true;
|
|
17271
|
+
void this.getPublicWebLink().openUnattended(OUTSIDE_LINK_OWNER).catch(() => "failed").then(outcome => {
|
|
17272
|
+
this.outsideOpening = false;
|
|
17273
|
+
if (outcome === "opened") {
|
|
17274
|
+
this.logger.info({ attempt: n + 1 }, "Outside link opened");
|
|
17275
|
+
return;
|
|
17276
|
+
}
|
|
17277
|
+
if (outcome === "busy" && this.outsideLink())
|
|
17278
|
+
return; // already open (ours)
|
|
17279
|
+
if (this.shuttingDown || !this.outsideOn())
|
|
17280
|
+
return;
|
|
17281
|
+
if (n + 1 < delays.length) {
|
|
17282
|
+
this.outsideAttempt = outsideTimer(() => attempt(n + 1), delays[n + 1]);
|
|
17283
|
+
return;
|
|
17284
|
+
}
|
|
17285
|
+
this.logger.warn({ attempts: n + 1 }, "Outside link could not be opened; giving up until the next start or switch");
|
|
17286
|
+
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_failed")));
|
|
17287
|
+
});
|
|
17288
|
+
};
|
|
17289
|
+
this.outsideAttempt = outsideTimer(() => attempt(0), delays[0] ?? 0);
|
|
17290
|
+
}
|
|
17291
|
+
closeOutsideLink(reason) {
|
|
17292
|
+
this.outsideAttempt?.cancel();
|
|
17293
|
+
this.outsideAttempt = null;
|
|
17294
|
+
const open = this.publicWebLink?.openSignin();
|
|
17295
|
+
if (this.publicWebLink && (!open || isOutsideLinkOwner(open.owner)))
|
|
17296
|
+
void this.publicWebLink.close(reason);
|
|
17297
|
+
this.endOutsideSessions();
|
|
17298
|
+
}
|
|
17299
|
+
/** PublicWebLink.onClose for the outside link: what closed it decides what happens next (#3008 B). */
|
|
17300
|
+
outsideLinkClosed(reason) {
|
|
17301
|
+
if (this.shuttingDown || OUTSIDE_OWN_CLOSES.has(reason) || !this.outsideOn())
|
|
17302
|
+
return;
|
|
17303
|
+
// #3008 review (Prism P2 3): expiry first — its lifetime counts from the opening request, so it can run out while the
|
|
17304
|
+
// opening is still under way: the switch goes off then too, and that attempt's end (outsideOn false) retries nothing.
|
|
17305
|
+
if (reason === "expired") {
|
|
17306
|
+
// Its lifetime ran out: the switch goes off (only this computer turns it on again — a session from outside can't).
|
|
17307
|
+
this.outsideAttempt?.cancel();
|
|
17308
|
+
this.outsideAttempt = null;
|
|
17309
|
+
if (this.webRemoteAccess()?.setOutside(false) === "unsaved")
|
|
17310
|
+
this.logger.warn("Outside link expired; the switch's Off could not be saved yet (retried)");
|
|
17311
|
+
this.endOutsideSessions();
|
|
17312
|
+
this.logger.info("Outside link expired; connections from outside off");
|
|
17313
|
+
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_expired")));
|
|
17314
|
+
return;
|
|
17315
|
+
}
|
|
17316
|
+
if (this.outsideOpening)
|
|
17317
|
+
return; // that attempt's own failure: its retry
|
|
17318
|
+
// Dropped (the tunnel exited, the gateway failed, the configuration changed): reopened up to twice.
|
|
17319
|
+
this.logger.info({ reason }, "Outside link closed; reopening");
|
|
17320
|
+
this.openOutsideLink(OUTSIDE_LINK_RETRY_MS);
|
|
17321
|
+
}
|
|
17322
|
+
/**
|
|
17323
|
+
* #3008 (auth-api POST /auth/totp): a sign-in through the outside link with the authenticator's code. "ok": the caller
|
|
17324
|
+
* makes the session (SessionRecord.remote) and then calls authorizeOutsideSession. Each wrong code counts; a wait that
|
|
17325
|
+
* starts is a Notice; a code that matches a step just outside the window logs how far this clock seems to be off.
|
|
17326
|
+
*/
|
|
17327
|
+
outsideSignIn(exposureId, code) {
|
|
17328
|
+
const link = this.outsideLink(), store = this.webRemoteAccess();
|
|
17329
|
+
if (!link || !store || !exposureId || link.exposureId !== exposureId)
|
|
17330
|
+
return { kind: "closed" };
|
|
17331
|
+
const v = store.verify(code);
|
|
17332
|
+
if (v.kind === "ok")
|
|
17333
|
+
return { kind: "ok" };
|
|
17334
|
+
if (v.kind === "paused")
|
|
17335
|
+
return v;
|
|
17336
|
+
if (v.kind === "not_enrolled")
|
|
17337
|
+
return { kind: "closed" };
|
|
17338
|
+
if (v.kind === "unavailable")
|
|
17339
|
+
return v;
|
|
17340
|
+
if (v.skewSteps !== null)
|
|
17341
|
+
this.logger.warn({ offsetSeconds: v.skewSteps * TOTP_STEP_SECONDS }, "A wrong authenticator code matched a nearby time step: this computer's clock may be off");
|
|
17342
|
+
if (v.paused) {
|
|
17343
|
+
this.logger.warn({ waitMs: v.retryAfterMs }, "Sign-in from outside paused after wrong codes");
|
|
17344
|
+
this.recordWebNotice("fleet", t("outside.notice_paused", outsideDuration(v.retryAfterMs), v.wrong));
|
|
17345
|
+
return { kind: "paused", retryAfterMs: v.retryAfterMs };
|
|
17346
|
+
}
|
|
17347
|
+
return { kind: "wrong" };
|
|
17348
|
+
}
|
|
17349
|
+
/**
|
|
17350
|
+
* #3008: a session just made by outsideSignIn's "ok" is this computer's admin for its authorization's life. False: it
|
|
17351
|
+
* could not be admitted (its handle not saved, #3015 review r3) — the caller ends it.
|
|
17352
|
+
*/
|
|
17353
|
+
authorizeOutsideSession(session, peer, label) {
|
|
17354
|
+
const store = this.sessionAuthorizationStore(), link = this.outsideLink();
|
|
17355
|
+
if (!store || session.remote !== "totp" || session.owner || !link || session.exposureId !== link.exposureId)
|
|
17356
|
+
return false;
|
|
17357
|
+
if (!this.webRemoteAccess()?.admit(session.handle)) {
|
|
17358
|
+
this.logger.warn({ handle: session.handle }, "Session from outside not admitted: it could not be saved");
|
|
17359
|
+
return false;
|
|
17360
|
+
}
|
|
17361
|
+
const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
|
|
17362
|
+
const check = evaluateWebAuthority({
|
|
17363
|
+
session, gateway: true, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
17364
|
+
isAdmin: () => false,
|
|
17365
|
+
authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
|
|
17366
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
17367
|
+
hostOperator: true, outside: link,
|
|
17368
|
+
});
|
|
17369
|
+
if (check.refusal) {
|
|
17370
|
+
this.logger.info({ handle: session.handle, refusal: check.refusal }, "Session from outside not authorized");
|
|
17371
|
+
return false;
|
|
17372
|
+
}
|
|
17373
|
+
{
|
|
17374
|
+
const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
|
|
17375
|
+
store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
|
|
17376
|
+
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
17377
|
+
this.logger.info({ handle: session.handle, durable }, "Session from outside authorized (this computer's admin)");
|
|
17378
|
+
}
|
|
17379
|
+
this.recordWebNotice("fleet", t("outside.notice_signed_in", label));
|
|
17380
|
+
return true;
|
|
17381
|
+
}
|
|
15802
17382
|
authorityStateStore = null;
|
|
15803
17383
|
/**
|
|
15804
17384
|
* #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
|
|
@@ -15819,6 +17399,10 @@ export class FleetManager {
|
|
|
15819
17399
|
return true;
|
|
15820
17400
|
if (session.surface === "local")
|
|
15821
17401
|
return false;
|
|
17402
|
+
// #3008 review (Prism P2 4): signed in from outside with the authenticator's code — its opt-in is the outside
|
|
17403
|
+
// switch, not these options (the authority judges it so): it keeps its authorization.
|
|
17404
|
+
if (session.remote === "totp")
|
|
17405
|
+
return false;
|
|
15822
17406
|
const peer = a.peer ?? "named_link";
|
|
15823
17407
|
return (!!state.namedLinkClosed && peer === "named_link") || (!!state.quickLinkClosed && peer === "quick_link");
|
|
15824
17408
|
};
|
|
@@ -15968,7 +17552,7 @@ export class FleetManager {
|
|
|
15968
17552
|
authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
|
|
15969
17553
|
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
15970
17554
|
// #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
|
|
15971
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
17555
|
+
hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
|
|
15972
17556
|
});
|
|
15973
17557
|
return r.refusal;
|
|
15974
17558
|
}
|
|
@@ -16834,6 +18418,8 @@ export class FleetManager {
|
|
|
16834
18418
|
publication.retirePending = true;
|
|
16835
18419
|
}
|
|
16836
18420
|
async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
|
|
18421
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
18422
|
+
return;
|
|
16837
18423
|
// Post first, retire after (see the tail of this method). Retiring up front
|
|
16838
18424
|
// meant that from the delete until the new message came back — a chat API
|
|
16839
18425
|
// round trip, and every reply goes through here — the instance had NO live
|
|
@@ -17366,6 +18952,14 @@ export class FleetManager {
|
|
|
17366
18952
|
}
|
|
17367
18953
|
/** Interrupt an instance's current generation (cancel button / /cancel). */
|
|
17368
18954
|
cancelInstance(instanceName) {
|
|
18955
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
18956
|
+
this.cancelPendingDeliveries(instanceName);
|
|
18957
|
+
const cancelled = this.nativeInstanceRuntime().cancel(instanceName);
|
|
18958
|
+
this.lastInboundMsg.delete(instanceName);
|
|
18959
|
+
for (const message of this.webChatHistory.cancelPending(instanceName))
|
|
18960
|
+
this.emitSseEvent("delivery", { instance: instanceName, messageId: message.messageId, delivery: message.delivery });
|
|
18961
|
+
return cancelled !== null;
|
|
18962
|
+
}
|
|
17369
18963
|
const daemon = this.daemons.get(instanceName);
|
|
17370
18964
|
if (!daemon)
|
|
17371
18965
|
return false;
|
|
@@ -18583,36 +20177,7 @@ export class FleetManager {
|
|
|
18583
20177
|
"antigravity": ".agents/AGENTS.md",
|
|
18584
20178
|
"mock": "CLAUDE.md",
|
|
18585
20179
|
};
|
|
18586
|
-
static GENERAL_INSTRUCTIONS =
|
|
18587
|
-
|
|
18588
|
-
You are the fleet coordinator — the central entry point for this AgEnD fleet.
|
|
18589
|
-
You route tasks, manage instances, enforce policies, and synthesize results.
|
|
18590
|
-
Do NOT modify project files directly — delegate file changes to the project's instance.
|
|
18591
|
-
You CAN write code snippets, explain code, and answer technical questions directly.
|
|
18592
|
-
|
|
18593
|
-
## Task Routing
|
|
18594
|
-
|
|
18595
|
-
- **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
|
|
18596
|
-
- **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
|
|
18597
|
-
- **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
|
|
18598
|
-
|
|
18599
|
-
Instance discovery: start with list_instances(); follow the guidance in its response. Prefer reuse; never duplicate a running instance.
|
|
18600
|
-
|
|
18601
|
-
## Reply Contract
|
|
18602
|
-
|
|
18603
|
-
Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
|
|
18604
|
-
|
|
18605
|
-
## After Restart
|
|
18606
|
-
|
|
18607
|
-
BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
|
|
18608
|
-
|
|
18609
|
-
## Playbooks (on-demand skills)
|
|
18610
|
-
|
|
18611
|
-
Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
|
|
18612
|
-
- **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
|
|
18613
|
-
- **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
|
|
18614
|
-
Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
|
|
18615
|
-
`;
|
|
20180
|
+
static GENERAL_INSTRUCTIONS = GENERAL_ROLE_INSTRUCTIONS;
|
|
18616
20181
|
/** Ensure the general instance has its project instructions file + knowledge */
|
|
18617
20182
|
ensureGeneralInstructions(workDir, backendName, instanceName) {
|
|
18618
20183
|
measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
|
|
@@ -19634,6 +21199,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19634
21199
|
: model,
|
|
19635
21200
|
});
|
|
19636
21201
|
const fleetInstance = this.fleetConfig?.instances[instanceName];
|
|
21202
|
+
if (isWindowsInstance(fleetInstance))
|
|
21203
|
+
return fleetInstance.model?.trim() ? done(fleetInstance.model.trim(), "instance") : done("default", "unresolved", "this CLI does not report a default");
|
|
19637
21204
|
if (fleetInstance) {
|
|
19638
21205
|
if (fleetInstance.model?.trim())
|
|
19639
21206
|
return done(fleetInstance.model.trim(), "instance");
|
|
@@ -19715,7 +21282,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19715
21282
|
if (this.fleetConfig?.defaults?.backend)
|
|
19716
21283
|
backends.add(this.fleetConfig.defaults.backend);
|
|
19717
21284
|
for (const inst of Object.values(this.fleetConfig?.instances ?? {}))
|
|
19718
|
-
if (inst.backend)
|
|
21285
|
+
if (!isWindowsInstance(inst) && inst.backend)
|
|
19719
21286
|
backends.add(inst.backend);
|
|
19720
21287
|
for (const ch of this.classicChannels?.getAll() ?? [])
|
|
19721
21288
|
if (ch.backend)
|
|
@@ -19730,6 +21297,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19730
21297
|
// here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
|
|
19731
21298
|
const profiles = new Set();
|
|
19732
21299
|
for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
|
|
21300
|
+
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
21301
|
+
continue;
|
|
19733
21302
|
if (this.backendNameForInstance(name) !== "claude-code")
|
|
19734
21303
|
continue;
|
|
19735
21304
|
const profile = this.catalogProfile("claude-code", name);
|
|
@@ -19750,6 +21319,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19750
21319
|
* instead of presenting an old list as a fresh one.
|
|
19751
21320
|
*/
|
|
19752
21321
|
async getModelOptionsWithSource(instanceName, opts = {}) {
|
|
21322
|
+
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
21323
|
+
return { models: (await this.listModelCatalog({ instanceName })).models, source: "cache" };
|
|
19753
21324
|
const backendName = this.backendNameForInstance(instanceName);
|
|
19754
21325
|
const profile = this.catalogProfile(backendName, instanceName);
|
|
19755
21326
|
const cached = this.readCliEnv(backendName, profile);
|
|
@@ -19805,6 +21376,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19805
21376
|
*/
|
|
19806
21377
|
async listModelCatalog(opts = {}) {
|
|
19807
21378
|
const { instanceName } = opts;
|
|
21379
|
+
if (instanceName && isWindowsInstance(this.fleetConfig?.instances[instanceName])) {
|
|
21380
|
+
const config = this.fleetConfig.instances[instanceName], backend = config.backend ?? "codex";
|
|
21381
|
+
// Detection is admin-owned. A read never launches a provider or falls back to this host's account.
|
|
21382
|
+
let inventory = null;
|
|
21383
|
+
try {
|
|
21384
|
+
inventory = await this.runtimeWindowsExecutor().probe({ force: false, machineId: config.machine_id });
|
|
21385
|
+
}
|
|
21386
|
+
catch { /* offline remains unavailable */ }
|
|
21387
|
+
const row = inventory?.clis.find(value => value.id === backend);
|
|
21388
|
+
return { backend, scope: "instance", instance: instanceName, current_model: config.model || null,
|
|
21389
|
+
models: (row?.models ?? []).map(value => ({ id: value.id, label: value.name ?? value.id, ...(value.description ? { description: value.description } : {}) })),
|
|
21390
|
+
source: row?.models?.length ? "cache" : "fallback",
|
|
21391
|
+
...(inventory?.probedAt ? { probed_at: new Date(inventory.probedAt).toISOString() } : {}),
|
|
21392
|
+
note: "Windows runtime's last detected inventory; no coordinator account or live probe is used." };
|
|
21393
|
+
}
|
|
19808
21394
|
if (instanceName) {
|
|
19809
21395
|
const backend = this.backendNameForInstance(instanceName);
|
|
19810
21396
|
const resolved = this.resolveInstanceModel(instanceName);
|
|
@@ -19840,6 +21426,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19840
21426
|
}
|
|
19841
21427
|
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
19842
21428
|
async instanceScopedModels(instanceName, backend) {
|
|
21429
|
+
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
21430
|
+
return [];
|
|
19843
21431
|
try {
|
|
19844
21432
|
const inst = this.fleetConfig?.instances?.[instanceName];
|
|
19845
21433
|
const instanceDir = this.getInstanceDir(instanceName);
|
|
@@ -20477,6 +22065,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20477
22065
|
}
|
|
20478
22066
|
/** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
|
|
20479
22067
|
instanceEffortMetadata(instanceName) {
|
|
22068
|
+
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22069
|
+
return { strategy: "unsupported", levels: [] };
|
|
20480
22070
|
const backend = this.backendNameForInstance(instanceName);
|
|
20481
22071
|
let model = null;
|
|
20482
22072
|
if (backend === "antigravity") {
|
|
@@ -20487,6 +22077,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20487
22077
|
}
|
|
20488
22078
|
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
20489
22079
|
resolveInstanceEffort(instanceName) {
|
|
22080
|
+
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22081
|
+
return { effort: null, source: "unset" };
|
|
20490
22082
|
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
20491
22083
|
if (own)
|
|
20492
22084
|
return { effort: own, source: "instance" };
|
|
@@ -20524,9 +22116,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20524
22116
|
* instance respawns.
|
|
20525
22117
|
*/
|
|
20526
22118
|
async applyEffort(instanceName, requested, opts) {
|
|
20527
|
-
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
22119
|
+
// #1490 row 27 (main): a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
20528
22120
|
if (opts?.current && !opts.current())
|
|
20529
22121
|
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
22122
|
+
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22123
|
+
return t("effort.unsupported", this.backendNameForInstance(instanceName));
|
|
20530
22124
|
const level = requested.trim().toLowerCase();
|
|
20531
22125
|
const backendName = this.backendNameForInstance(instanceName);
|
|
20532
22126
|
let strategy = "unsupported";
|
|
@@ -20574,6 +22168,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20574
22168
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
20575
22169
|
return t("model.invalid_chars");
|
|
20576
22170
|
}
|
|
22171
|
+
const native = this.fleetConfig?.instances[instanceName];
|
|
22172
|
+
if (isWindowsInstance(native)) {
|
|
22173
|
+
if (!isNativePaneInstance(native))
|
|
22174
|
+
return WINDOWS_CONFIG_REFUSED;
|
|
22175
|
+
const catalog = await this.listModelCatalog({ instanceName });
|
|
22176
|
+
if (!catalog.models.some(row => row.id === model))
|
|
22177
|
+
return t("model.unknown_name", model, catalog.models.map(row => row.id).join(", "));
|
|
22178
|
+
const id = native.native_agent_id, machine = native.machine_id;
|
|
22179
|
+
await this.updateNativePaneConfig(instanceName, { model }, () => {
|
|
22180
|
+
const now = this.fleetConfig?.instances[instanceName];
|
|
22181
|
+
return isNativePaneInstance(now) && now.native_agent_id === id && now.machine_id === machine;
|
|
22182
|
+
});
|
|
22183
|
+
return t("model.restart_success", instanceName, model);
|
|
22184
|
+
}
|
|
20577
22185
|
const backendName = this.backendNameForInstance(instanceName);
|
|
20578
22186
|
let strategy = "restart";
|
|
20579
22187
|
try {
|
|
@@ -20762,6 +22370,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20762
22370
|
* profile's, when it is named and has one), claude's account catalog included. Read only: never a probe.
|
|
20763
22371
|
*/
|
|
20764
22372
|
modelCatalogFor(backend, instance) {
|
|
22373
|
+
if (instance && isWindowsInstance(this.fleetConfig?.instances[instance]))
|
|
22374
|
+
return [];
|
|
20765
22375
|
if (!KNOWN_BACKENDS.includes(backend) || backend === "mock")
|
|
20766
22376
|
return [];
|
|
20767
22377
|
const own = instance && Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, instance) ? instance
|
|
@@ -20775,6 +22385,40 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20775
22385
|
}
|
|
20776
22386
|
return out;
|
|
20777
22387
|
}
|
|
22388
|
+
/** Host-default fallback only: Grok has no modeled profile home. Never construct a backend or start a probe. */
|
|
22389
|
+
async cliModelCatalogFor(backend, instance) {
|
|
22390
|
+
if (backend !== "grok")
|
|
22391
|
+
return [];
|
|
22392
|
+
const fleet = this.fleetConfig;
|
|
22393
|
+
const configured = instance && Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) ? fleet.instances[instance] : undefined;
|
|
22394
|
+
const classic = this.classicChannels;
|
|
22395
|
+
const room = instance && !configured ? classic?.getAll().find(channel => channel.instanceName === instance) : undefined;
|
|
22396
|
+
if (instance && !configured && !room)
|
|
22397
|
+
return [];
|
|
22398
|
+
const daemon = room ? this.daemons.get(instance) : undefined;
|
|
22399
|
+
const grokHome = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
22400
|
+
const stillHostDefault = () => {
|
|
22401
|
+
if (this.fleetConfig !== fleet || (process.env.GROK_HOME?.trim() || join(homedir(), ".grok")) !== grokHome)
|
|
22402
|
+
return false;
|
|
22403
|
+
if (configured && (isWindowsInstance(configured) || !Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) || fleet.instances[instance] !== configured))
|
|
22404
|
+
return false;
|
|
22405
|
+
if (room && (this.classicChannels !== classic || classic?.getAll().find(channel => channel.instanceName === instance) !== room || this.daemons.get(instance) !== daemon))
|
|
22406
|
+
return false;
|
|
22407
|
+
// The ordinary profile helpers deliberately ignore Grok: it has no credential-home mapping yet.
|
|
22408
|
+
// Check raw declarations, including invalid ones, rather than mistaking them for the host login.
|
|
22409
|
+
const defaults = fleet?.defaults?.backend_options;
|
|
22410
|
+
const profiles = configured
|
|
22411
|
+
? [configured.backend_options?.grok?.credential_profile, defaults?.grok?.credential_profile]
|
|
22412
|
+
: room
|
|
22413
|
+
? [mergeBackendOptions(defaults, room.backendOptions)?.grok?.credential_profile, daemon?.getConfigSnapshot?.().backend_options?.grok?.credential_profile]
|
|
22414
|
+
: [defaults?.grok?.credential_profile];
|
|
22415
|
+
return profiles.every(profile => profile === undefined || profile === null || profile === "");
|
|
22416
|
+
};
|
|
22417
|
+
if (!stillHostDefault())
|
|
22418
|
+
return [];
|
|
22419
|
+
const models = await readGrokModelCache(grokHome);
|
|
22420
|
+
return stillHostDefault() ? models : [];
|
|
22421
|
+
}
|
|
20778
22422
|
/**
|
|
20779
22423
|
* #2072 (#2078 review): the snapshot as THIS instance would see it. Its sign-in on a backend is its own credential
|
|
20780
22424
|
* profile's when it has one there — the host's default login says nothing about it. A profile's login is a file check
|
|
@@ -21433,6 +23077,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21433
23077
|
* settles, so a later genuine stop (after a restart) still does the work.
|
|
21434
23078
|
*/
|
|
21435
23079
|
stopAll() {
|
|
23080
|
+
this.lifecycle.cancelNativeRecovery?.();
|
|
23081
|
+
this.nativeMcpStore?.closeAll();
|
|
23082
|
+
this.runtimeExecutor?.close();
|
|
23083
|
+
this.runtimeExecutor = null;
|
|
23084
|
+
this.fleetWindowsRuntime = null;
|
|
23085
|
+
this.runtimeHub?.close();
|
|
23086
|
+
this.runtimeHub = null;
|
|
23087
|
+
this.localPaneRelay?.stop();
|
|
23088
|
+
this.localPaneRelay = null;
|
|
23089
|
+
closeWindowsBridge(this);
|
|
21436
23090
|
this.stopAllInFlight ??= this.doStopAll().finally(() => {
|
|
21437
23091
|
// #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
|
|
21438
23092
|
if (this.tmuxExecHelperTimer) {
|
|
@@ -21511,6 +23165,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21511
23165
|
const settingsControlStopped = settingsControl?.close();
|
|
21512
23166
|
this.publicLinkAutoOpen?.stop();
|
|
21513
23167
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
23168
|
+
this.webRemoteAccessState?.close(); // #3008: an unsaved switch gets its last write
|
|
21514
23169
|
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
21515
23170
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
21516
23171
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
@@ -21893,6 +23548,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21893
23548
|
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
21894
23549
|
this.loadedFleetSource = previousSource;
|
|
21895
23550
|
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
23551
|
+
this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
|
|
21896
23552
|
this.logger.error({
|
|
21897
23553
|
oldCount,
|
|
21898
23554
|
newCount,
|
|
@@ -23528,7 +25184,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23528
25184
|
const topicMode = daemonTopicMode(fleet);
|
|
23529
25185
|
const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
|
|
23530
25186
|
// Phase 1: generals first
|
|
23531
|
-
const restartEntries = Object.entries(fleet.instances);
|
|
25187
|
+
const restartEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
|
|
23532
25188
|
const restartGenerals = restartEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
23533
25189
|
const restartOthers = restartEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
23534
25190
|
for (const [name, cfg] of restartGenerals) {
|
|
@@ -23982,6 +25638,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23982
25638
|
return;
|
|
23983
25639
|
}
|
|
23984
25640
|
const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
25641
|
+
if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
|
|
25642
|
+
&& this.runtimeCoordinator().handlePublicRequest(req, res, new URL(req.url ?? "/", `http://localhost:${port}`)))
|
|
25643
|
+
return;
|
|
23985
25644
|
// Branding is public, after Host admission: automatic favicon requests
|
|
23986
25645
|
// must not become noisy web-token 401s in the browser console.
|
|
23987
25646
|
// #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
|
|
@@ -24008,7 +25667,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24008
25667
|
if (decision.reason === "no-credential" && req.method === "GET"
|
|
24009
25668
|
&& String(req.headers.accept ?? "").includes("text/html")
|
|
24010
25669
|
&& isWebPageNavigation(requestPath)) {
|
|
24011
|
-
|
|
25670
|
+
const link = gatewayRequestContext(req);
|
|
25671
|
+
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
|
|
24012
25672
|
return;
|
|
24013
25673
|
}
|
|
24014
25674
|
res.writeHead(decision.status);
|
|
@@ -24365,6 +26025,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24365
26025
|
if (!this.webToken || !this.webSessions)
|
|
24366
26026
|
this.initializeWebAuthTokens();
|
|
24367
26027
|
const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
|
|
26028
|
+
server.on("upgrade", (req, socket, head) => {
|
|
26029
|
+
if (!this.shuttingDown && remoteRuntimeEnabled() && isHostAllowed(req.headers.host, allowedHostNames(this.fleetConfig))
|
|
26030
|
+
&& this.runtimeCoordinator().handleUpgrade(req, socket, head))
|
|
26031
|
+
return;
|
|
26032
|
+
socket.destroy();
|
|
26033
|
+
});
|
|
24368
26034
|
// #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
|
|
24369
26035
|
// more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
|
|
24370
26036
|
// keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
|
|
@@ -24589,9 +26255,36 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24589
26255
|
this.logger.debug({ err }, "bots unreadable (getUiStatus)");
|
|
24590
26256
|
}
|
|
24591
26257
|
const instances = names.map(name => {
|
|
26258
|
+
const nativeConfig = this.fleetConfig?.instances[name];
|
|
26259
|
+
if (isNativeExecInstance(nativeConfig)) {
|
|
26260
|
+
let runtime = null;
|
|
26261
|
+
let agent = null;
|
|
26262
|
+
try {
|
|
26263
|
+
runtime = this.nativeInstanceRuntime();
|
|
26264
|
+
agent = runtime.get(name);
|
|
26265
|
+
}
|
|
26266
|
+
catch { /* A damaged native profile is a crashed native row, never a Linux spawn. */ }
|
|
26267
|
+
const display_name = nativeConfig.display_name || agent?.name || name;
|
|
26268
|
+
const backend = nativeConfig.backend || agent?.cli || "codex";
|
|
26269
|
+
return { name, display_name, description: nativeConfig.description || undefined, tags: nativeConfig.tags || [],
|
|
26270
|
+
teams: instanceTeams(this.fleetConfig?.teams, name), runtime: "windows", native_agent_id: nativeConfig.native_agent_id,
|
|
26271
|
+
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
26272
|
+
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}),
|
|
26273
|
+
native_blocked: !agent || agent.blocked === true,
|
|
26274
|
+
native_busy: runtime?.busy(name) ?? false,
|
|
26275
|
+
backend,
|
|
26276
|
+
model: nativeConfig.model || "default", model_source: nativeConfig.model ? "instance" : "cli-default",
|
|
26277
|
+
status: agent && runtime ? runtime.status(name) : "crashed", state: agent && runtime ? runtime.execution(name) : null, execution_state: agent && runtime ? runtime.execution(name) : null,
|
|
26278
|
+
interaction: null, interaction_summary: null, context_pct: null, cost: null,
|
|
26279
|
+
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined,
|
|
26280
|
+
avatar: avatarFor({ name, displayName: display_name }),
|
|
26281
|
+
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: nativeProviderMcpSupported(backend), channels: false, terminal: false, attachments: false, usage: false, send_while_busy: false },
|
|
26282
|
+
...(!agent ? { runtime_error: "Windows profile is missing or invalid; restore its saved conversation before using this instance" }
|
|
26283
|
+
: agent.blocked ? { runtime_error: "Windows process outcome is unknown; verify it stopped before recovery" } : {}) };
|
|
26284
|
+
}
|
|
24592
26285
|
// #1956: read once for this instance; its cost, context and model come from it.
|
|
24593
|
-
const statusline = readStatuslineJson(this.dataDir, name);
|
|
24594
|
-
const cost = statusline?.cost?.total_cost_usd ?? 0;
|
|
26286
|
+
const statusline = isNativePaneInstance(nativeConfig) ? undefined : readStatuslineJson(this.dataDir, name);
|
|
26287
|
+
const cost = isNativePaneInstance(nativeConfig) ? null : statusline?.cost?.total_cost_usd ?? 0;
|
|
24595
26288
|
// Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
|
|
24596
26289
|
const classic = classicOnly.includes(name);
|
|
24597
26290
|
const backend = classic
|
|
@@ -24599,7 +26292,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24599
26292
|
: (this.fleetConfig?.instances[name]?.backend
|
|
24600
26293
|
?? this.fleetConfig?.defaults?.backend
|
|
24601
26294
|
?? "claude-code");
|
|
24602
|
-
const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
|
|
26295
|
+
const { context } = isNativePaneInstance(nativeConfig) ? { context: null } : resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
|
|
24603
26296
|
// context_pct: null when unavailable, not 0
|
|
24604
26297
|
const context_pct = context ?? null;
|
|
24605
26298
|
// Model: Only Claude Code has live statusline; others use the effective resolver.
|
|
@@ -24630,6 +26323,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24630
26323
|
}
|
|
24631
26324
|
return {
|
|
24632
26325
|
name,
|
|
26326
|
+
general_topic: nativeConfig?.general_topic === true,
|
|
26327
|
+
...(isNativePaneInstance(nativeConfig) ? { runtime: "windows", native_adapter: "pane", native_agent_id: nativeConfig.native_agent_id,
|
|
26328
|
+
...(() => {
|
|
26329
|
+
try {
|
|
26330
|
+
const profile = this.nativeInstanceRuntime().get(name), bound = this.daemons.get(name);
|
|
26331
|
+
return { native_recovery: profile?.paneRecovery ?? (profile?.paneLaunchId && bound?.usesNativePane?.() && !bound.nativePaneCurrent()
|
|
26332
|
+
? { kind: "exit-unconfirmed", launchId: profile.paneLaunchId } : null) };
|
|
26333
|
+
}
|
|
26334
|
+
catch {
|
|
26335
|
+
return { native_recovery: null };
|
|
26336
|
+
}
|
|
26337
|
+
})(),
|
|
26338
|
+
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
|
|
26339
|
+
commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
|
|
26340
|
+
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
26341
|
+
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
|
|
24633
26342
|
display_name: identity.display_name || undefined,
|
|
24634
26343
|
avatar: image.avatar,
|
|
24635
26344
|
...(image.accent ? { accent: image.accent } : {}),
|
|
@@ -24652,6 +26361,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24652
26361
|
role: identity.role || undefined,
|
|
24653
26362
|
tags: identity.tags,
|
|
24654
26363
|
teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
|
|
26364
|
+
// #3008 G2: a web-only fleet's General (the agent its owner ticked) — the sidebar pins it first.
|
|
26365
|
+
...(this.isWebOnlyFleet() && this.fleetConfig?.instances[name]?.general_topic === true ? { general: true } : {}),
|
|
24655
26366
|
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
|
|
24656
26367
|
// #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
|
|
24657
26368
|
last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
|
|
@@ -24681,6 +26392,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24681
26392
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
24682
26393
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|
|
24683
26394
|
connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
|
|
26395
|
+
windows_cli: localWindowsCliEnabled(),
|
|
24684
26396
|
// #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
|
|
24685
26397
|
prefs_rev: this.webPrefs.rev(),
|
|
24686
26398
|
// #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
|
|
@@ -24690,6 +26402,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24690
26402
|
host_memory: this.hostMemoryIndicator(),
|
|
24691
26403
|
// #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
|
|
24692
26404
|
...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
|
|
26405
|
+
// alpha.2 (M-2300, #2997 review): agents' requests changed — the owner's page reads GET /ui/agent-proposals again.
|
|
26406
|
+
...(this.isWebOnlyFleet() ? { agent_proposals_rev: this.agentProposalsRev } : {}),
|
|
24693
26407
|
// #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
|
|
24694
26408
|
...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
|
|
24695
26409
|
};
|
|
@@ -25132,4 +26846,12 @@ load();
|
|
|
25132
26846
|
</script>
|
|
25133
26847
|
</body>
|
|
25134
26848
|
</html>`;
|
|
26849
|
+
/** alpha.2 (M-2300): the agent an applied Windows request created (its name, for the page's "Open chat"), or null. */
|
|
26850
|
+
export function agentNameOf(view) {
|
|
26851
|
+
const r = view.state === "applied" ? view.outcome?.result : null;
|
|
26852
|
+
if (!r || typeof r !== "object")
|
|
26853
|
+
return null;
|
|
26854
|
+
const o = r, name = o.name ?? o.instance_name ?? (o.agent && typeof o.agent === "object" ? o.agent.name : undefined);
|
|
26855
|
+
return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ? name : null;
|
|
26856
|
+
}
|
|
25135
26857
|
//# sourceMappingURL=fleet-manager.js.map
|