@songsid/agend 2.3.5-alpha.5 → 2.3.5-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/README.zh-TW.md +0 -2
- package/THIRD_PARTY_NOTICES.md +0 -68
- package/dist/agent-endpoint.d.ts +1 -5
- package/dist/agent-endpoint.js +2 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +1 -1
- package/dist/agent-file-read.js.map +1 -1
- package/dist/auth-api.d.ts +0 -48
- package/dist/auth-api.js +4 -250
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +2 -0
- package/dist/authority/session-authorizations.js +7 -1
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +2 -12
- package/dist/authority/web-authority.js +4 -25
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/claude-code.d.ts +12 -15
- package/dist/backend/claude-code.js +133 -152
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +0 -3
- package/dist/backend/codex-footer-hints.js +0 -14
- package/dist/backend/codex-footer-hints.js.map +1 -1
- package/dist/backend/codex-package.d.ts +2 -5
- package/dist/backend/codex-package.js +5 -1
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex-quota-history.js +19 -4
- package/dist/backend/codex-quota-history.js.map +1 -1
- package/dist/backend/codex.d.ts +115 -142
- package/dist/backend/codex.js +1094 -1187
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +0 -2
- package/dist/backend/grok.js +0 -22
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +2 -7
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-tmux-guard.d.ts +21 -0
- package/dist/bundled-tmux-guard.js +96 -0
- package/dist/bundled-tmux-guard.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +0 -2
- package/dist/channel/mcp-tools.js +2 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +2 -0
- package/dist/classic-channel-manager.js +5 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +30 -127
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +13 -74
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -17
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +5 -72
- package/dist/daemon.js +275 -869
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +3 -15
- package/dist/delivery-consumed-watch.js +1 -23
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.d.ts +0 -5
- package/dist/delivery-outbox.js +4 -32
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +42 -2
- package/dist/delivery-reconciliation.js +164 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/doctor.js +9 -20
- package/dist/doctor.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 +0 -2
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +9 -498
- package/dist/fleet-manager.js +202 -2849
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +0 -7
- package/dist/general-instance.js +3 -20
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +18 -50
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +2 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +2 -2
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +10 -10
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +13 -23
- package/dist/instance-config-impact.js +4 -8
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +3 -22
- package/dist/instance-lifecycle.js +176 -393
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +0 -2
- package/dist/instructions.js +0 -6
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +2 -147
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.d.ts +7 -8
- package/dist/login-flows.js +1 -24
- package/dist/login-flows.js.map +1 -1
- package/dist/ls-rows.js +4 -3
- package/dist/ls-rows.js.map +1 -1
- package/dist/muse-usage-relay.d.ts +2 -0
- package/dist/muse-usage-relay.js +5 -3
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/needs-you-hub.d.ts +0 -5
- package/dist/needs-you-hub.js +2 -18
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +2 -45
- package/dist/needs-you.js +2 -26
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share-page.js +3 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share.d.ts +0 -11
- package/dist/office-share.js +0 -14
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -30
- package/dist/outbound-handlers.js +4 -109
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -71
- package/dist/outbound-schemas.js +14 -36
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-quota-history.d.ts +0 -2
- package/dist/pane-quota-history.js +0 -20
- package/dist/pane-quota-history.js.map +1 -1
- package/dist/pause-marker.d.ts +1 -1
- package/dist/pause-marker.js +1 -1
- package/dist/pause-marker.js.map +1 -1
- package/dist/public-web-gateway.js +0 -16
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +0 -11
- package/dist/public-web-link.js +0 -13
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js +5 -9
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +0 -6
- package/dist/quickstart.js +6 -44
- package/dist/quickstart.js.map +1 -1
- package/dist/quota-notice-config.d.ts +22 -0
- package/dist/quota-notice-config.js +41 -0
- package/dist/quota-notice-config.js.map +1 -0
- package/dist/scheduler/task-board-text.js +1 -1
- package/dist/scheduler/task-board-text.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +0 -4
- package/dist/scheduler/task-board.js +3 -9
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-installer.d.ts +2 -7
- package/dist/service-installer.js +10 -13
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +0 -12
- package/dist/settings-api.js +7 -487
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +0 -7
- package/dist/settings-baseline.js +1 -3
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +7 -35
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +1 -16
- package/dist/settings-confirmation.js +9 -47
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.d.ts +0 -17
- package/dist/settings-effect.js +1 -155
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +0 -4
- package/dist/settings-http-confirmation.js +3 -43
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/settings-request-capability.d.ts +0 -2
- package/dist/settings-request-capability.js +0 -4
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/settings-transaction.d.ts +0 -4
- package/dist/settings-transaction.js +0 -9
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-form.js +3 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +0 -3
- package/dist/setup-i18n.js.map +1 -1
- package/dist/setup-wizard.js +2 -6
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-tool.d.ts +1 -2
- package/dist/task-board-tool.js +6 -62
- package/dist/task-board-tool.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -2
- package/dist/tmux-control.js +4 -4
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.js +1 -32
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -3
- package/dist/tmux-manager.js +36 -45
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.js +3 -4
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js +1 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +5 -9
- 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 +0 -3
- package/dist/transcript-monitor.js +51 -27
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +0 -5
- package/dist/transcript-sources.js +79 -8
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +3 -29
- package/dist/ui/app.html +8 -1
- package/dist/ui/backend-signin.js +21 -39
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-md-strings.js +9 -3
- package/dist/ui/chat-render.js +670 -3
- package/dist/ui/chat-store.js +3 -14
- package/dist/ui/chat-strings.js +285 -3
- package/dist/ui/chat-thread.js +969 -3
- package/dist/ui/chat-time.js +80 -3
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/fleet-office-pack.d.ts +0 -9
- package/dist/ui/fleet-office-pack.js +4 -76
- package/dist/ui/fleet-office-skin.js +5 -23
- package/dist/ui/fleet-office-store.js +0 -5
- package/dist/ui/fleet-office.js +6 -173
- package/dist/ui/fleet-task-card.js +4 -8
- package/dist/ui/fleet-task-common.js +2 -2
- package/dist/ui/fleet-tasks.js +39 -125
- package/dist/ui/html-attachment.js +40 -3
- package/dist/ui/office-share-page.js +2 -4
- package/dist/ui/panel-chat.js +75 -90
- package/dist/ui/panel-details.js +2 -3
- package/dist/ui/panel-fleet.js +18 -105
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +4 -162
- package/dist/ui/panel-notices.js +4 -3
- package/dist/ui/panel-settings.js +202 -281
- package/dist/ui/preview-pages.js +1 -1
- package/dist/ui/preview-panel.js +13 -21
- package/dist/ui/preview-versions.js +1 -1
- package/dist/ui/preview.js +476 -3
- package/dist/ui/search-dialog.js +3 -4
- package/dist/ui/settings-appearance.js +23 -122
- package/dist/ui/settings-catalog.js +10 -37
- package/dist/ui/settings-confirm.js +1 -3
- package/dist/ui/settings-dialogs.js +20 -48
- package/dist/ui/settings-guests.js +14 -27
- package/dist/ui/settings-model.js +1 -10
- package/dist/ui/settings-quota-notice.js +100 -0
- package/dist/ui/settings-registry.js +5 -15
- package/dist/ui/settings-strings.js +32 -105
- package/dist/ui/settings-web-authority.js +13 -18
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +3 -12
- package/dist/ui/shared/app-session.js +2 -6
- package/dist/ui/shared/app-shell.js +26 -87
- package/dist/ui/shared/app-store.js +2 -3
- package/dist/ui/shared/app-stream.js +1 -3
- package/dist/ui/shared/app.css +5 -1368
- package/dist/ui/shared/app.js +5 -17
- package/dist/ui/shared/header-tools.js +9 -9
- package/dist/ui/shared/instance-nav.js +7 -34
- package/dist/ui/shared/instance-switch.js +3 -4
- package/dist/ui/shared/loading.css +37 -0
- package/dist/ui/shared/signin.js +6 -211
- package/dist/ui/shared/tokens.css +0 -1
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/ui-menu.js +10 -30
- package/dist/ui/shared/ui-states.js +14 -3
- package/dist/ui/shared/vendor/vendor.json +2 -19
- package/dist/ui/shared/view-strings.js +0 -8
- package/dist/ui/signin.html +5 -23
- package/dist/unattended-install.js +1 -2
- package/dist/unattended-install.js.map +1 -1
- package/dist/usage/providers.d.ts +5 -0
- package/dist/usage/providers.js +6 -2
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/quota-notices.d.ts +38 -0
- package/dist/usage/quota-notices.js +160 -0
- package/dist/usage/quota-notices.js.map +1 -0
- package/dist/usage/usage-api.d.ts +1 -0
- package/dist/usage/usage-api.js +16 -0
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.js +7 -5
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +2 -80
- package/dist/web-api.js +10 -513
- package/dist/web-api.js.map +1 -1
- package/dist/web-backend-login.d.ts +1 -23
- package/dist/web-backend-login.js +17 -11
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-chat-history.d.ts +2 -19
- package/dist/web-chat-history.js +3 -72
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-office-skins.d.ts +0 -9
- package/dist/web-office-skins.js +1 -16
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-only-fleet.d.ts +0 -20
- package/dist/web-only-fleet.js +0 -21
- package/dist/web-only-fleet.js.map +1 -1
- package/dist/web-session.d.ts +0 -42
- package/dist/web-session.js +0 -9
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -3
- package/dist/web-settings-patch.js +1 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -10
- package/dist/web-shell-routes.js +2 -14
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-terminal.d.ts +30 -4
- package/dist/web-terminal.js +6 -9
- package/dist/web-terminal.js.map +1 -1
- package/dist/yaml-shared-nodes.js +3 -8
- package/dist/yaml-shared-nodes.js.map +1 -1
- package/launcher/agend-install.cjs +1 -8
- package/launcher/postinstall.cjs +2 -27
- package/package.json +3 -10
- package/templates/launchd.plist.ejs +1 -3
- package/dist/accounts-view.d.ts +0 -97
- package/dist/accounts-view.js +0 -116
- package/dist/accounts-view.js.map +0 -1
- package/dist/agent-runtime-api.d.ts +0 -4
- package/dist/agent-runtime-api.js +0 -59
- package/dist/agent-runtime-api.js.map +0 -1
- package/dist/agent-runtime-change.d.ts +0 -76
- package/dist/agent-runtime-change.js +0 -114
- package/dist/agent-runtime-change.js.map +0 -1
- package/dist/agent-workspace.d.ts +0 -93
- package/dist/agent-workspace.js +0 -353
- package/dist/agent-workspace.js.map +0 -1
- package/dist/backend/grok-model-cache.d.ts +0 -7
- package/dist/backend/grok-model-cache.js +0 -65
- package/dist/backend/grok-model-cache.js.map +0 -1
- package/dist/backend/native-pane.d.ts +0 -11
- package/dist/backend/native-pane.js +0 -59
- package/dist/backend/native-pane.js.map +0 -1
- package/dist/build-provenance-cli.d.ts +0 -3
- package/dist/build-provenance-cli.js +0 -11
- package/dist/build-provenance-cli.js.map +0 -1
- package/dist/build-provenance.d.ts +0 -20
- package/dist/build-provenance.js +0 -67
- package/dist/build-provenance.js.map +0 -1
- package/dist/build-provenance.json +0 -1
- package/dist/claude-auth-status.d.ts +0 -48
- package/dist/claude-auth-status.js +0 -149
- package/dist/claude-auth-status.js.map +0 -1
- package/dist/fleet-windows-runtime.d.ts +0 -98
- package/dist/fleet-windows-runtime.js +0 -467
- package/dist/fleet-windows-runtime.js.map +0 -1
- package/dist/general-binding.d.ts +0 -85
- package/dist/general-binding.js +0 -298
- package/dist/general-binding.js.map +0 -1
- package/dist/general-host-owner.d.ts +0 -8
- package/dist/general-host-owner.js +0 -26
- package/dist/general-host-owner.js.map +0 -1
- package/dist/general-role.d.ts +0 -5
- package/dist/general-role.js +0 -27
- package/dist/general-role.js.map +0 -1
- package/dist/hub/admission.d.ts +0 -28
- package/dist/hub/admission.js +0 -62
- package/dist/hub/admission.js.map +0 -1
- package/dist/hub/attachments/limits.d.ts +0 -20
- package/dist/hub/attachments/limits.js +0 -48
- package/dist/hub/attachments/limits.js.map +0 -1
- package/dist/hub/attachments/store.d.ts +0 -116
- package/dist/hub/attachments/store.js +0 -246
- package/dist/hub/attachments/store.js.map +0 -1
- package/dist/hub/audit/index.d.ts +0 -62
- package/dist/hub/audit/index.js +0 -344
- package/dist/hub/audit/index.js.map +0 -1
- package/dist/hub/browser-certificate.d.ts +0 -38
- package/dist/hub/browser-certificate.js +0 -247
- package/dist/hub/browser-certificate.js.map +0 -1
- package/dist/hub/buttons-model.d.ts +0 -9
- package/dist/hub/buttons-model.js +0 -42
- package/dist/hub/buttons-model.js.map +0 -1
- package/dist/hub/buttons.d.ts +0 -54
- package/dist/hub/buttons.js +0 -346
- package/dist/hub/buttons.js.map +0 -1
- package/dist/hub/catalog.d.ts +0 -74
- package/dist/hub/catalog.js +0 -195
- package/dist/hub/catalog.js.map +0 -1
- package/dist/hub/client.d.ts +0 -69
- package/dist/hub/client.js +0 -300
- package/dist/hub/client.js.map +0 -1
- package/dist/hub/compose/member.d.ts +0 -139
- package/dist/hub/compose/member.js +0 -450
- package/dist/hub/compose/member.js.map +0 -1
- package/dist/hub/compose/server.d.ts +0 -77
- package/dist/hub/compose/server.js +0 -349
- package/dist/hub/compose/server.js.map +0 -1
- package/dist/hub/compose/views.d.ts +0 -12
- package/dist/hub/compose/views.js +0 -4
- package/dist/hub/compose/views.js.map +0 -1
- package/dist/hub/db.d.ts +0 -56
- package/dist/hub/db.js +0 -188
- package/dist/hub/db.js.map +0 -1
- package/dist/hub/directory/config.d.ts +0 -39
- package/dist/hub/directory/config.js +0 -108
- package/dist/hub/directory/config.js.map +0 -1
- package/dist/hub/directory/directory.d.ts +0 -18
- package/dist/hub/directory/directory.js +0 -320
- package/dist/hub/directory/directory.js.map +0 -1
- package/dist/hub/directory/index.d.ts +0 -7
- package/dist/hub/directory/index.js +0 -4
- package/dist/hub/directory/index.js.map +0 -1
- package/dist/hub/directory/profiles.d.ts +0 -41
- package/dist/hub/directory/profiles.js +0 -90
- package/dist/hub/directory/profiles.js.map +0 -1
- package/dist/hub/directory/types.d.ts +0 -71
- package/dist/hub/directory/types.js +0 -14
- package/dist/hub/directory/types.js.map +0 -1
- package/dist/hub/execution/cli-provider.d.ts +0 -23
- package/dist/hub/execution/cli-provider.js +0 -341
- package/dist/hub/execution/cli-provider.js.map +0 -1
- package/dist/hub/execution/index.d.ts +0 -13
- package/dist/hub/execution/index.js +0 -8
- package/dist/hub/execution/index.js.map +0 -1
- package/dist/hub/execution/model-broker.d.ts +0 -24
- package/dist/hub/execution/model-broker.js +0 -208
- package/dist/hub/execution/model-broker.js.map +0 -1
- package/dist/hub/execution/provider-setup.d.ts +0 -11
- package/dist/hub/execution/provider-setup.js +0 -21
- package/dist/hub/execution/provider-setup.js.map +0 -1
- package/dist/hub/execution/sandbox-runner.d.ts +0 -1
- package/dist/hub/execution/sandbox-runner.js +0 -221
- package/dist/hub/execution/sandbox-runner.js.map +0 -1
- package/dist/hub/execution/service.d.ts +0 -136
- package/dist/hub/execution/service.js +0 -459
- package/dist/hub/execution/service.js.map +0 -1
- package/dist/hub/execution/store.d.ts +0 -61
- package/dist/hub/execution/store.js +0 -182
- package/dist/hub/execution/store.js.map +0 -1
- package/dist/hub/execution/views.d.ts +0 -20
- package/dist/hub/execution/views.js +0 -48
- package/dist/hub/execution/views.js.map +0 -1
- package/dist/hub/http/compose.d.ts +0 -20
- package/dist/hub/http/compose.js +0 -518
- package/dist/hub/http/compose.js.map +0 -1
- package/dist/hub/http/core.d.ts +0 -49
- package/dist/hub/http/core.js +0 -124
- package/dist/hub/http/core.js.map +0 -1
- package/dist/hub/http/deps.d.ts +0 -61
- package/dist/hub/http/deps.js +0 -2
- package/dist/hub/http/deps.js.map +0 -1
- package/dist/hub/http/schema.d.ts +0 -14
- package/dist/hub/http/schema.js +0 -80
- package/dist/hub/http/schema.js.map +0 -1
- package/dist/hub/identity.d.ts +0 -32
- package/dist/hub/identity.js +0 -80
- package/dist/hub/identity.js.map +0 -1
- package/dist/hub/init/browser-certificate.d.ts +0 -9
- package/dist/hub/init/browser-certificate.js +0 -78
- package/dist/hub/init/browser-certificate.js.map +0 -1
- package/dist/hub/init/browser-tls.d.ts +0 -18
- package/dist/hub/init/browser-tls.js +0 -62
- package/dist/hub/init/browser-tls.js.map +0 -1
- package/dist/hub/init/cli.d.ts +0 -3
- package/dist/hub/init/cli.js +0 -83
- package/dist/hub/init/cli.js.map +0 -1
- package/dist/hub/init/directory-certificate.d.ts +0 -24
- package/dist/hub/init/directory-certificate.js +0 -222
- package/dist/hub/init/directory-certificate.js.map +0 -1
- package/dist/hub/init/existing-fleet.d.ts +0 -6
- package/dist/hub/init/existing-fleet.js +0 -58
- package/dist/hub/init/existing-fleet.js.map +0 -1
- package/dist/hub/init/handoff.d.ts +0 -8
- package/dist/hub/init/handoff.js +0 -36
- package/dist/hub/init/handoff.js.map +0 -1
- package/dist/hub/init/identity.d.ts +0 -5
- package/dist/hub/init/identity.js +0 -24
- package/dist/hub/init/identity.js.map +0 -1
- package/dist/hub/init/initializer.d.ts +0 -24
- package/dist/hub/init/initializer.js +0 -385
- package/dist/hub/init/initializer.js.map +0 -1
- package/dist/hub/init/inventory.d.ts +0 -17
- package/dist/hub/init/inventory.js +0 -33
- package/dist/hub/init/inventory.js.map +0 -1
- package/dist/hub/init/layout.d.ts +0 -32
- package/dist/hub/init/layout.js +0 -53
- package/dist/hub/init/layout.js.map +0 -1
- package/dist/hub/init/network.d.ts +0 -18
- package/dist/hub/init/network.js +0 -104
- package/dist/hub/init/network.js.map +0 -1
- package/dist/hub/init/plan-digest.d.ts +0 -5
- package/dist/hub/init/plan-digest.js +0 -47
- package/dist/hub/init/plan-digest.js.map +0 -1
- package/dist/hub/init/prompt.d.ts +0 -26
- package/dist/hub/init/prompt.js +0 -110
- package/dist/hub/init/prompt.js.map +0 -1
- package/dist/hub/init/secret-file.d.ts +0 -1
- package/dist/hub/init/secret-file.js +0 -59
- package/dist/hub/init/secret-file.js.map +0 -1
- package/dist/hub/init/text.d.ts +0 -127
- package/dist/hub/init/text.js +0 -247
- package/dist/hub/init/text.js.map +0 -1
- package/dist/hub/init/types.d.ts +0 -114
- package/dist/hub/init/types.js +0 -9
- package/dist/hub/init/types.js.map +0 -1
- package/dist/hub/init/wizard.d.ts +0 -34
- package/dist/hub/init/wizard.js +0 -554
- package/dist/hub/init/wizard.js.map +0 -1
- package/dist/hub/initialization.d.ts +0 -23
- package/dist/hub/initialization.js +0 -28
- package/dist/hub/initialization.js.map +0 -1
- package/dist/hub/member/agent-mode.d.ts +0 -6
- package/dist/hub/member/agent-mode.js +0 -23
- package/dist/hub/member/agent-mode.js.map +0 -1
- package/dist/hub/member/attachments.d.ts +0 -29
- package/dist/hub/member/attachments.js +0 -107
- package/dist/hub/member/attachments.js.map +0 -1
- package/dist/hub/member/exposure-api.d.ts +0 -87
- package/dist/hub/member/exposure-api.js +0 -64
- package/dist/hub/member/exposure-api.js.map +0 -1
- package/dist/hub/member/exposures.d.ts +0 -123
- package/dist/hub/member/exposures.js +0 -282
- package/dist/hub/member/exposures.js.map +0 -1
- package/dist/hub/member/pairing-api.d.ts +0 -85
- package/dist/hub/member/pairing-api.js +0 -385
- package/dist/hub/member/pairing-api.js.map +0 -1
- package/dist/hub/member/quota.d.ts +0 -20
- package/dist/hub/member/quota.js +0 -98
- package/dist/hub/member/quota.js.map +0 -1
- package/dist/hub/member-journal.d.ts +0 -104
- package/dist/hub/member-journal.js +0 -336
- package/dist/hub/member-journal.js.map +0 -1
- package/dist/hub/member-reconciliation.d.ts +0 -14
- package/dist/hub/member-reconciliation.js +0 -97
- package/dist/hub/member-reconciliation.js.map +0 -1
- package/dist/hub/mount.d.ts +0 -39
- package/dist/hub/mount.js +0 -123
- package/dist/hub/mount.js.map +0 -1
- package/dist/hub/pairing-client.d.ts +0 -17
- package/dist/hub/pairing-client.js +0 -108
- package/dist/hub/pairing-client.js.map +0 -1
- package/dist/hub/pairing.d.ts +0 -65
- package/dist/hub/pairing.js +0 -211
- package/dist/hub/pairing.js.map +0 -1
- package/dist/hub/policy.d.ts +0 -63
- package/dist/hub/policy.js +0 -147
- package/dist/hub/policy.js.map +0 -1
- package/dist/hub/preview.d.ts +0 -32
- package/dist/hub/preview.js +0 -64
- package/dist/hub/preview.js.map +0 -1
- package/dist/hub/private-files.d.ts +0 -5
- package/dist/hub/private-files.js +0 -44
- package/dist/hub/private-files.js.map +0 -1
- package/dist/hub/profile.d.ts +0 -59
- package/dist/hub/profile.js +0 -73
- package/dist/hub/profile.js.map +0 -1
- package/dist/hub/protocol.d.ts +0 -779
- package/dist/hub/protocol.js +0 -93
- package/dist/hub/protocol.js.map +0 -1
- package/dist/hub/quota.d.ts +0 -24
- package/dist/hub/quota.js +0 -112
- package/dist/hub/quota.js.map +0 -1
- package/dist/hub/representative.d.ts +0 -141
- package/dist/hub/representative.js +0 -528
- package/dist/hub/representative.js.map +0 -1
- package/dist/hub/retention.d.ts +0 -17
- package/dist/hub/retention.js +0 -45
- package/dist/hub/retention.js.map +0 -1
- package/dist/hub/rooms.d.ts +0 -120
- package/dist/hub/rooms.js +0 -535
- package/dist/hub/rooms.js.map +0 -1
- package/dist/hub/runtime.d.ts +0 -106
- package/dist/hub/runtime.js +0 -298
- package/dist/hub/runtime.js.map +0 -1
- package/dist/hub/server.d.ts +0 -56
- package/dist/hub/server.js +0 -202
- package/dist/hub/server.js.map +0 -1
- package/dist/hub/service-start.d.ts +0 -17
- package/dist/hub/service-start.js +0 -109
- package/dist/hub/service-start.js.map +0 -1
- package/dist/hub/sessions.d.ts +0 -53
- package/dist/hub/sessions.js +0 -196
- package/dist/hub/sessions.js.map +0 -1
- package/dist/hub/socket.d.ts +0 -24
- package/dist/hub/socket.js +0 -79
- package/dist/hub/socket.js.map +0 -1
- package/dist/hub/store.d.ts +0 -77
- package/dist/hub/store.js +0 -88
- package/dist/hub/store.js.map +0 -1
- package/dist/hub/tls.d.ts +0 -31
- package/dist/hub/tls.js +0 -135
- package/dist/hub/tls.js.map +0 -1
- package/dist/hub/types.d.ts +0 -401
- package/dist/hub/types.js +0 -2
- package/dist/hub/types.js.map +0 -1
- package/dist/hub/validation.d.ts +0 -33
- package/dist/hub/validation.js +0 -136
- package/dist/hub/validation.js.map +0 -1
- package/dist/hub/version-floor.d.ts +0 -10
- package/dist/hub/version-floor.js +0 -45
- package/dist/hub/version-floor.js.map +0 -1
- package/dist/hub-browser-certificate-policy.d.ts +0 -31
- package/dist/hub-browser-certificate-policy.js +0 -25
- package/dist/hub-browser-certificate-policy.js.map +0 -1
- package/dist/hub-membership-settings.d.ts +0 -6
- package/dist/hub-membership-settings.js +0 -21
- package/dist/hub-membership-settings.js.map +0 -1
- package/dist/hub-mode.d.ts +0 -15
- package/dist/hub-mode.js +0 -17
- package/dist/hub-mode.js.map +0 -1
- package/dist/hub-pairing-summary.d.ts +0 -48
- package/dist/hub-pairing-summary.js +0 -36
- package/dist/hub-pairing-summary.js.map +0 -1
- package/dist/hub-report-target.d.ts +0 -32
- package/dist/hub-report-target.js +0 -33
- package/dist/hub-report-target.js.map +0 -1
- package/dist/hub-retention-policy.d.ts +0 -14
- package/dist/hub-retention-policy.js +0 -15
- package/dist/hub-retention-policy.js.map +0 -1
- package/dist/launchd-service-domain.d.ts +0 -5
- package/dist/launchd-service-domain.js +0 -21
- package/dist/launchd-service-domain.js.map +0 -1
- package/dist/local-pane-endpoint.d.ts +0 -46
- package/dist/local-pane-endpoint.js +0 -189
- package/dist/local-pane-endpoint.js.map +0 -1
- package/dist/local-pane-launcher.d.ts +0 -2
- package/dist/local-pane-launcher.js +0 -38
- package/dist/local-pane-launcher.js.map +0 -1
- package/dist/local-pane-stage.ps1 +0 -101
- package/dist/model-catalog-defaults.d.ts +0 -14
- package/dist/model-catalog-defaults.js +0 -20
- package/dist/model-catalog-defaults.js.map +0 -1
- package/dist/native-agent-admission.d.ts +0 -7
- package/dist/native-agent-admission.js +0 -26
- package/dist/native-agent-admission.js.map +0 -1
- package/dist/native-daemon-client.d.ts +0 -13
- package/dist/native-daemon-client.js +0 -28
- package/dist/native-daemon-client.js.map +0 -1
- package/dist/native-instance-api.d.ts +0 -13
- package/dist/native-instance-api.js +0 -217
- package/dist/native-instance-api.js.map +0 -1
- package/dist/native-instance-input.d.ts +0 -87
- package/dist/native-instance-input.js +0 -59
- package/dist/native-instance-input.js.map +0 -1
- package/dist/native-instance-registry.d.ts +0 -38
- package/dist/native-instance-registry.js +0 -129
- package/dist/native-instance-registry.js.map +0 -1
- package/dist/native-mcp.d.ts +0 -43
- package/dist/native-mcp.js +0 -291
- package/dist/native-mcp.js.map +0 -1
- package/dist/native-model-catalog.d.ts +0 -5
- package/dist/native-model-catalog.js +0 -76
- package/dist/native-model-catalog.js.map +0 -1
- package/dist/native-pane-crash-budget.d.ts +0 -20
- package/dist/native-pane-crash-budget.js +0 -60
- package/dist/native-pane-crash-budget.js.map +0 -1
- package/dist/native-pane-create.d.ts +0 -26
- package/dist/native-pane-create.js +0 -53
- package/dist/native-pane-create.js.map +0 -1
- package/dist/native-pane-lifecycle.d.ts +0 -73
- package/dist/native-pane-lifecycle.js +0 -239
- package/dist/native-pane-lifecycle.js.map +0 -1
- package/dist/native-pane-manager.d.ts +0 -59
- package/dist/native-pane-manager.js +0 -592
- package/dist/native-pane-manager.js.map +0 -1
- package/dist/native-pane-output-log.d.ts +0 -15
- package/dist/native-pane-output-log.js +0 -108
- package/dist/native-pane-output-log.js.map +0 -1
- package/dist/native-pane-reconciliation.d.ts +0 -4
- package/dist/native-pane-reconciliation.js +0 -40
- package/dist/native-pane-reconciliation.js.map +0 -1
- package/dist/native-pane-recovery.d.ts +0 -12
- package/dist/native-pane-recovery.js +0 -10
- package/dist/native-pane-recovery.js.map +0 -1
- package/dist/native-pane-tools.d.ts +0 -2
- package/dist/native-pane-tools.js +0 -14
- package/dist/native-pane-tools.js.map +0 -1
- package/dist/office-talk.d.ts +0 -26
- package/dist/office-talk.js +0 -50
- package/dist/office-talk.js.map +0 -1
- package/dist/pane-port.d.ts +0 -118
- package/dist/pane-port.js +0 -2
- package/dist/pane-port.js.map +0 -1
- package/dist/runtime-api.d.ts +0 -10
- package/dist/runtime-api.js +0 -189
- package/dist/runtime-api.js.map +0 -1
- package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +0 -2
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +0 -19
- package/dist/runtime-assets/@xterm/addon-serialize/package.json +0 -29
- package/dist/runtime-assets/@xterm/headless/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +0 -2
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +0 -46
- package/dist/runtime-assets/@xterm/headless/package.json +0 -29
- package/dist/runtime-assets/node-pty/CONPTY-LICENSE +0 -21
- package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +0 -503
- package/dist/runtime-assets/node-pty/LICENSE +0 -69
- package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +0 -16
- package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +0 -47
- package/dist/runtime-assets/node-pty/lib/index.js +0 -52
- package/dist/runtime-assets/node-pty/lib/interfaces.js +0 -7
- package/dist/runtime-assets/node-pty/lib/shared/conout.js +0 -11
- package/dist/runtime-assets/node-pty/lib/terminal.js +0 -190
- package/dist/runtime-assets/node-pty/lib/types.js +0 -7
- package/dist/runtime-assets/node-pty/lib/unixTerminal.js +0 -346
- package/dist/runtime-assets/node-pty/lib/utils.js +0 -39
- package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +0 -125
- package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +0 -320
- package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +0 -199
- package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +0 -22
- package/dist/runtime-assets/node-pty/package.json +0 -64
- 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-build-info.d.ts +0 -9
- package/dist/runtime-build-info.js +0 -59
- package/dist/runtime-build-info.js.map +0 -1
- package/dist/runtime-bundle.d.ts +0 -22
- package/dist/runtime-bundle.js +0 -158
- package/dist/runtime-bundle.js.map +0 -1
- package/dist/runtime-cli.d.ts +0 -14
- package/dist/runtime-cli.js +0 -271
- package/dist/runtime-cli.js.map +0 -1
- package/dist/runtime-client-store.d.ts +0 -75
- package/dist/runtime-client-store.js +0 -372
- package/dist/runtime-client-store.js.map +0 -1
- package/dist/runtime-client.d.ts +0 -174
- package/dist/runtime-client.js +0 -1010
- package/dist/runtime-client.js.map +0 -1
- package/dist/runtime-connection.d.ts +0 -22
- package/dist/runtime-connection.js +0 -93
- package/dist/runtime-connection.js.map +0 -1
- package/dist/runtime-coordinator.d.ts +0 -98
- package/dist/runtime-coordinator.js +0 -1089
- package/dist/runtime-coordinator.js.map +0 -1
- package/dist/runtime-credential-profiles.d.ts +0 -78
- package/dist/runtime-credential-profiles.js +0 -280
- package/dist/runtime-credential-profiles.js.map +0 -1
- package/dist/runtime-general-protocol.d.ts +0 -22
- package/dist/runtime-general-protocol.js +0 -57
- package/dist/runtime-general-protocol.js.map +0 -1
- package/dist/runtime-general-workspace.d.ts +0 -17
- package/dist/runtime-general-workspace.js +0 -213
- package/dist/runtime-general-workspace.js.map +0 -1
- package/dist/runtime-install.d.ts +0 -50
- package/dist/runtime-install.js +0 -186
- package/dist/runtime-install.js.map +0 -1
- package/dist/runtime-pane-assets.d.ts +0 -17
- package/dist/runtime-pane-assets.js +0 -23
- package/dist/runtime-pane-assets.js.map +0 -1
- package/dist/runtime-pane-boot.d.ts +0 -9
- package/dist/runtime-pane-boot.js +0 -47
- package/dist/runtime-pane-boot.js.map +0 -1
- package/dist/runtime-pane-codex-index.d.ts +0 -10
- package/dist/runtime-pane-codex-index.js +0 -151
- package/dist/runtime-pane-codex-index.js.map +0 -1
- package/dist/runtime-pane-config.d.ts +0 -16
- package/dist/runtime-pane-config.js +0 -54
- package/dist/runtime-pane-config.js.map +0 -1
- package/dist/runtime-pane-host.d.ts +0 -63
- package/dist/runtime-pane-host.js +0 -467
- package/dist/runtime-pane-host.js.map +0 -1
- package/dist/runtime-pane-job-owner.d.ts +0 -1
- package/dist/runtime-pane-job-owner.js +0 -29
- package/dist/runtime-pane-job-owner.js.map +0 -1
- package/dist/runtime-pane-job.ps1 +0 -116
- package/dist/runtime-pane-link.d.ts +0 -47
- package/dist/runtime-pane-link.js +0 -337
- package/dist/runtime-pane-link.js.map +0 -1
- package/dist/runtime-pane-mcp-protocol.d.ts +0 -32
- package/dist/runtime-pane-mcp-protocol.js +0 -39
- package/dist/runtime-pane-mcp-protocol.js.map +0 -1
- package/dist/runtime-pane-mcp-relay.d.ts +0 -18
- package/dist/runtime-pane-mcp-relay.js +0 -80
- package/dist/runtime-pane-mcp-relay.js.map +0 -1
- package/dist/runtime-pane-mcp.d.ts +0 -55
- package/dist/runtime-pane-mcp.js +0 -153
- package/dist/runtime-pane-mcp.js.map +0 -1
- package/dist/runtime-pane-native.d.ts +0 -75
- package/dist/runtime-pane-native.js +0 -114
- package/dist/runtime-pane-native.js.map +0 -1
- package/dist/runtime-pane-output.d.ts +0 -19
- package/dist/runtime-pane-output.js +0 -53
- package/dist/runtime-pane-output.js.map +0 -1
- package/dist/runtime-pane-port.d.ts +0 -144
- package/dist/runtime-pane-port.js +0 -574
- package/dist/runtime-pane-port.js.map +0 -1
- package/dist/runtime-pane-private.d.ts +0 -14
- package/dist/runtime-pane-private.js +0 -51
- package/dist/runtime-pane-private.js.map +0 -1
- package/dist/runtime-pane-private.ps1 +0 -14
- package/dist/runtime-pane-process.d.ts +0 -4
- package/dist/runtime-pane-process.js +0 -117
- package/dist/runtime-pane-process.js.map +0 -1
- package/dist/runtime-pane-protocol.d.ts +0 -182
- package/dist/runtime-pane-protocol.js +0 -217
- package/dist/runtime-pane-protocol.js.map +0 -1
- package/dist/runtime-pane-quota-history.d.ts +0 -6
- package/dist/runtime-pane-quota-history.js +0 -44
- package/dist/runtime-pane-quota-history.js.map +0 -1
- package/dist/runtime-pane-screen.d.ts +0 -19
- package/dist/runtime-pane-screen.js +0 -76
- package/dist/runtime-pane-screen.js.map +0 -1
- package/dist/runtime-pane-service.d.ts +0 -16
- package/dist/runtime-pane-service.js +0 -256
- package/dist/runtime-pane-service.js.map +0 -1
- package/dist/runtime-pane-stdio-protocol.d.ts +0 -54
- package/dist/runtime-pane-stdio-protocol.js +0 -94
- package/dist/runtime-pane-stdio-protocol.js.map +0 -1
- package/dist/runtime-pane-stdio.d.ts +0 -3
- package/dist/runtime-pane-stdio.js +0 -123
- package/dist/runtime-pane-stdio.js.map +0 -1
- package/dist/runtime-pane-store.d.ts +0 -33
- package/dist/runtime-pane-store.js +0 -164
- package/dist/runtime-pane-store.js.map +0 -1
- package/dist/runtime-pane-transcript-protocol.d.ts +0 -43
- package/dist/runtime-pane-transcript-protocol.js +0 -58
- package/dist/runtime-pane-transcript-protocol.js.map +0 -1
- package/dist/runtime-pane-transcript.d.ts +0 -50
- package/dist/runtime-pane-transcript.js +0 -666
- package/dist/runtime-pane-transcript.js.map +0 -1
- package/dist/runtime-pane-worker.d.ts +0 -1
- package/dist/runtime-pane-worker.js +0 -167
- package/dist/runtime-pane-worker.js.map +0 -1
- package/dist/runtime-protocol.d.ts +0 -177
- package/dist/runtime-protocol.js +0 -49
- package/dist/runtime-protocol.js.map +0 -1
- package/dist/runtime-switches.d.ts +0 -79
- package/dist/runtime-switches.js +0 -90
- package/dist/runtime-switches.js.map +0 -1
- package/dist/runtime-transcript-delta-reader.d.ts +0 -23
- package/dist/runtime-transcript-delta-reader.js +0 -74
- package/dist/runtime-transcript-delta-reader.js.map +0 -1
- package/dist/runtime-transcript-source.d.ts +0 -36
- package/dist/runtime-transcript-source.js +0 -189
- package/dist/runtime-transcript-source.js.map +0 -1
- package/dist/runtime-update-activity.d.ts +0 -21
- package/dist/runtime-update-activity.js +0 -51
- package/dist/runtime-update-activity.js.map +0 -1
- package/dist/runtime-update-artifact.d.ts +0 -31
- package/dist/runtime-update-artifact.js +0 -252
- package/dist/runtime-update-artifact.js.map +0 -1
- package/dist/runtime-update-install.d.ts +0 -31
- package/dist/runtime-update-install.js +0 -205
- package/dist/runtime-update-install.js.map +0 -1
- package/dist/runtime-update-journal.d.ts +0 -44
- package/dist/runtime-update-journal.js +0 -121
- package/dist/runtime-update-journal.js.map +0 -1
- package/dist/runtime-update-lock.d.ts +0 -48
- package/dist/runtime-update-lock.js +0 -217
- package/dist/runtime-update-lock.js.map +0 -1
- package/dist/runtime-update-owners.d.ts +0 -29
- package/dist/runtime-update-owners.js +0 -240
- package/dist/runtime-update-owners.js.map +0 -1
- package/dist/runtime-update-protocol.d.ts +0 -15
- package/dist/runtime-update-protocol.js +0 -13
- package/dist/runtime-update-protocol.js.map +0 -1
- package/dist/runtime-windows-bridge.d.ts +0 -42
- package/dist/runtime-windows-bridge.js +0 -167
- package/dist/runtime-windows-bridge.js.map +0 -1
- package/dist/service-drain.d.ts +0 -19
- package/dist/service-drain.js +0 -64
- package/dist/service-drain.js.map +0 -1
- package/dist/settings-route-lifetime.d.ts +0 -14
- package/dist/settings-route-lifetime.js +0 -48
- package/dist/settings-route-lifetime.js.map +0 -1
- package/dist/tmux-binary.d.ts +0 -59
- package/dist/tmux-binary.js +0 -200
- package/dist/tmux-binary.js.map +0 -1
- package/dist/tmux-pane-host.d.ts +0 -26
- package/dist/tmux-pane-host.js +0 -25
- package/dist/tmux-pane-host.js.map +0 -1
- package/dist/transcript-needles.d.ts +0 -42
- package/dist/transcript-needles.js +0 -166
- package/dist/transcript-needles.js.map +0 -1
- package/dist/transcript-records.d.ts +0 -21
- package/dist/transcript-records.js +0 -141
- package/dist/transcript-records.js.map +0 -1
- package/dist/ui/account-identity.js +0 -27
- package/dist/ui/accounts-view.js +0 -214
- package/dist/ui/agent-proposals.js +0 -90
- package/dist/ui/agent-runtime-settings.js +0 -68
- package/dist/ui/agent-runtime.js +0 -124
- package/dist/ui/cli-picker.js +0 -42
- package/dist/ui/credential-profile-select.js +0 -54
- package/dist/ui/fleet-office-dialogue.d.ts +0 -30
- package/dist/ui/fleet-office-dialogue.js +0 -382
- package/dist/ui/fleet-task-menu.js +0 -188
- package/dist/ui/fleet-task-stats.js +0 -124
- package/dist/ui/fleet-task-view.js +0 -340
- package/dist/ui/general-location.js +0 -71
- package/dist/ui/hub-agent-copy.js +0 -23
- package/dist/ui/hub-button-actions.js +0 -65
- package/dist/ui/hub-chat-store.js +0 -158
- package/dist/ui/hub-chat-strings.js +0 -77
- package/dist/ui/hub-client.js +0 -329
- package/dist/ui/hub-owner-strings.js +0 -51
- package/dist/ui/hub-provider-guidance.js +0 -31
- package/dist/ui/hub-quota.js +0 -85
- package/dist/ui/hub-strings.js +0 -162
- package/dist/ui/hub-switcher.js +0 -27
- package/dist/ui/native-instance-settings.js +0 -44
- package/dist/ui/native-pane-recovery.js +0 -45
- package/dist/ui/panel-hub.js +0 -619
- package/dist/ui/panel-runtimes.js +0 -425
- package/dist/ui/panel-windows-agent.js +0 -86
- package/dist/ui/panel-windows-cli.js +0 -189
- package/dist/ui/provider-marks.js +0 -83
- package/dist/ui/runtime-catalog.js +0 -55
- package/dist/ui/runtime-commands.d.ts +0 -4
- package/dist/ui/runtime-commands.js +0 -59
- package/dist/ui/settings-access-strings.js +0 -42
- package/dist/ui/settings-agent-filters.js +0 -94
- package/dist/ui/settings-build-version.js +0 -35
- package/dist/ui/settings-devices.js +0 -253
- package/dist/ui/settings-hub-browser-certificate.js +0 -74
- package/dist/ui/settings-hub-membership.js +0 -72
- package/dist/ui/settings-hub-retention.js +0 -52
- package/dist/ui/settings-hub.js +0 -196
- package/dist/ui/settings-layout.js +0 -242
- package/dist/ui/settings-outside.js +0 -242
- package/dist/ui/settings-shared-strings.js +0 -143
- package/dist/ui/settings-shared.js +0 -345
- package/dist/ui/settings-shortcuts.js +0 -65
- package/dist/ui/shared/accounts.css +0 -15
- package/dist/ui/shared/chat-composer.js +0 -68
- package/dist/ui/shared/chat-md-strings.js +0 -9
- package/dist/ui/shared/chat-render.js +0 -682
- package/dist/ui/shared/chat-strings.js +0 -333
- package/dist/ui/shared/chat-thread.js +0 -969
- package/dist/ui/shared/chat-time.js +0 -80
- package/dist/ui/shared/html-attachment.js +0 -42
- package/dist/ui/shared/hub-chat.css +0 -85
- package/dist/ui/shared/hub-owner.css +0 -33
- package/dist/ui/shared/hub-quota.css +0 -8
- package/dist/ui/shared/hub.css +0 -71
- package/dist/ui/shared/machine-name.js +0 -13
- package/dist/ui/shared/page-head.js +0 -8
- package/dist/ui/shared/preview.js +0 -476
- package/dist/ui/shared/qrcode.module.js +0 -2237
- package/dist/ui/shared/runtime-install.css +0 -26
- package/dist/ui/shared/settings-width.js +0 -31
- package/dist/ui/shared/side-rail.js +0 -15
- package/dist/ui/shared/task-board.css +0 -95
- package/dist/ui/shared/vendor/qrcode-generator.LICENSE +0 -27
- package/dist/ui/shared/workspace.css +0 -20
- package/dist/ui/windows-agents-nav.js +0 -2
- package/dist/ui/windows-cli-catalog.js +0 -108
- package/dist/ui/workspace-panel.js +0 -189
- package/dist/ui/workspace-strings.js +0 -35
- package/dist/web-device-login.d.ts +0 -153
- package/dist/web-device-login.js +0 -278
- package/dist/web-device-login.js.map +0 -1
- package/dist/web-remote-access.d.ts +0 -152
- package/dist/web-remote-access.js +0 -368
- package/dist/web-remote-access.js.map +0 -1
- package/dist/windows-agents.d.ts +0 -127
- package/dist/windows-agents.js +0 -491
- package/dist/windows-agents.js.map +0 -1
- package/dist/windows-cli-api.d.ts +0 -34
- package/dist/windows-cli-api.js +0 -185
- package/dist/windows-cli-api.js.map +0 -1
- package/dist/windows-cli-providers.d.ts +0 -106
- package/dist/windows-cli-providers.js +0 -424
- package/dist/windows-cli-providers.js.map +0 -1
- package/dist/windows-cli-worker.d.ts +0 -83
- package/dist/windows-cli-worker.js +0 -570
- package/dist/windows-cli-worker.js.map +0 -1
- package/dist/windows-cli.d.ts +0 -126
- package/dist/windows-cli.js +0 -416
- package/dist/windows-cli.js.map +0 -1
- package/dist/windows-native-mcp.d.ts +0 -19
- package/dist/windows-native-mcp.js +0 -143
- package/dist/windows-native-mcp.js.map +0 -1
- package/dist/windows-pane-mcp.d.ts +0 -26
- package/dist/windows-pane-mcp.js +0 -187
- package/dist/windows-pane-mcp.js.map +0 -1
- package/launcher/launchd-session-type.cjs +0 -14
- package/launcher/tmux-bundle.cjs +0 -247
- package/templates/runtime-assets/conpty-LICENSE +0 -21
- package/templates/runtime-assets/conpty-NOTICE.md +0 -503
- package/templates/runtime-assets/local-pane-stage.ps1 +0 -101
- package/templates/runtime-assets/runtime-pane-job.ps1 +0 -116
- package/templates/runtime-assets/runtime-pane-private.ps1 +0 -14
- package/templates/runtime-assets/xterm-LICENSE +0 -21
- package/templates/runtime-install/install.ps1 +0 -270
package/dist/fleet-manager.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { NativePaneRecoveryError } from "./native-pane-recovery.js";
|
|
5
|
-
import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
6
|
-
import { windowsAgentsFor } from "./windows-agents.js";
|
|
7
|
-
import { AgentRuntimeChanges } from "./agent-runtime-change.js";
|
|
1
|
+
import { QuotaNotices } from "./usage/quota-notices.js";
|
|
2
|
+
import { quotaNoticeRule } from "./quota-notice-config.js";
|
|
3
|
+
import { onUsageSnapshot } from "./usage/usage-api.js";
|
|
8
4
|
import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
9
5
|
import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
|
|
10
6
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
@@ -17,17 +13,10 @@ import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecovery
|
|
|
17
13
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
18
14
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
19
15
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
20
|
-
import {
|
|
21
|
-
import { prepareSettingsEffect } from "./settings-effect.js";
|
|
22
|
-
import { settingsDisplay } from "./settings-change.js";
|
|
23
|
-
import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
16
|
+
import { SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
24
17
|
import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
|
|
25
18
|
import { SettingsControlServer } from "./settings-control.js";
|
|
26
|
-
import {
|
|
27
|
-
import { GeneralManagedFileError } from "./runtime-general-protocol.js";
|
|
28
|
-
import { GeneralBindings, generalBindingRevision, parseGeneralSwitchInput, switchGeneralLocation } from "./general-binding.js";
|
|
29
|
-
import { confirmGeneralHostExit } from "./general-host-owner.js";
|
|
30
|
-
import { settingsFingerprint, SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
19
|
+
import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
31
20
|
import { performance } from "node:perf_hooks";
|
|
32
21
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
33
22
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
@@ -47,8 +36,6 @@ import { InstallStream } from "./install-stream.js";
|
|
|
47
36
|
import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
|
|
48
37
|
import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
|
|
49
38
|
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
50
|
-
import { OfficeTalk } from "./office-talk.js";
|
|
51
|
-
import { pairingSummary } from "./hub-pairing-summary.js";
|
|
52
39
|
import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
|
|
53
40
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
54
41
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
@@ -59,7 +46,7 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
59
46
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
|
|
60
47
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
61
48
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
62
|
-
import { freemem, totalmem, homedir
|
|
49
|
+
import { freemem, totalmem, homedir } from "node:os";
|
|
63
50
|
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
64
51
|
import { createServer } from "node:http";
|
|
65
52
|
import { Worker } from "node:worker_threads";
|
|
@@ -85,11 +72,6 @@ import { tmuxExecHelper } from "./tmux-exec-helper.js";
|
|
|
85
72
|
import { kiroLogGate } from "./kiro-log-gate.js";
|
|
86
73
|
const __filename = fileURLToPath(import.meta.url);
|
|
87
74
|
const __dirname = dirname(__filename);
|
|
88
|
-
// Describe this running program, not files an updater may replace later. No reads on dashboard polls.
|
|
89
|
-
const RUNNING_BUILD = readInstalledBuild();
|
|
90
|
-
if (RUNNING_BUILD.source)
|
|
91
|
-
Object.freeze(RUNNING_BUILD.source);
|
|
92
|
-
Object.freeze(RUNNING_BUILD);
|
|
93
75
|
/** Fallback access policy for a channel with no `access:` block — open (no gate). */
|
|
94
76
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
95
77
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
@@ -107,8 +89,7 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
|
107
89
|
import { TmuxManager } from "./tmux-manager.js";
|
|
108
90
|
import { AccessManager } from "./channel/access-manager.js";
|
|
109
91
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
110
|
-
import {
|
|
111
|
-
import { usesChannelAddress, TOOLS } from "./channel/mcp-tools.js";
|
|
92
|
+
import { usesChannelAddress } from "./channel/mcp-tools.js";
|
|
112
93
|
import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
|
|
113
94
|
import { formatFleetTime } from "./tz-utils.js";
|
|
114
95
|
import { createAdapter } from "./channel/factory.js";
|
|
@@ -119,7 +100,6 @@ import { createBackend } from "./backend/factory.js";
|
|
|
119
100
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
120
101
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
121
102
|
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
122
|
-
import { readGrokModelCache } from "./backend/grok-model-cache.js";
|
|
123
103
|
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
124
104
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
125
105
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
@@ -140,7 +120,6 @@ import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, che
|
|
|
140
120
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
141
121
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
142
122
|
import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
|
|
143
|
-
import { HUB_REPORT_TARGET } from "./hub-report-target.js";
|
|
144
123
|
import { backendArgs, backendStep, joinBackendArgs, modelNameCarried, planDisplay } from "./backend-command.js";
|
|
145
124
|
import { DeliveryOutbox, DEFAULT_REPLY_OBLIGATION_TTL_MS, isSilentScheduleDelivery } from "./delivery-outbox.js";
|
|
146
125
|
// The target Daemon can legitimately wait 30 minutes for a busy pane before
|
|
@@ -209,24 +188,11 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
209
188
|
return tasks;
|
|
210
189
|
}
|
|
211
190
|
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
212
|
-
import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
|
|
213
|
-
import { nativeCliVersionState } from "./runtime-pane-native.js";
|
|
214
|
-
import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
|
|
215
|
-
import { RuntimeInstallService } from "./runtime-install.js";
|
|
216
|
-
import { RUNTIME_CREDENTIAL_CAPABILITY } from "./runtime-credential-profiles.js";
|
|
217
|
-
import { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
218
|
-
import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
|
|
219
|
-
import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
|
|
220
|
-
import { NativeMcpSessions } from "./native-mcp.js";
|
|
221
|
-
import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
|
|
222
|
-
import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED, nativeCredentialProfile } from "./native-instance-registry.js";
|
|
223
|
-
import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
|
|
224
|
-
import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
|
|
225
191
|
import { LogService } from "./log-service.js";
|
|
226
192
|
import { viewHistory } from "./view-history.js";
|
|
227
193
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
228
194
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
229
|
-
import { WebChatDiskStore
|
|
195
|
+
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
230
196
|
import { SeenUsers, resolveMentions } from "./web-mentions.js";
|
|
231
197
|
import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
|
|
232
198
|
import { agendVersion } from "./agent-whats-new.js";
|
|
@@ -257,7 +223,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
|
|
|
257
223
|
import { parseEnvText } from "./env-file.js";
|
|
258
224
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
259
225
|
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
260
|
-
import { LOGIN_FLOWS, checkAuthStatus,
|
|
226
|
+
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
261
227
|
import { LoginSession } from "./login-manager.js";
|
|
262
228
|
import { runOnLoginShell } from "./login-shell.js";
|
|
263
229
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
@@ -324,10 +290,8 @@ import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefu
|
|
|
324
290
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
|
|
325
291
|
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
326
292
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
327
|
-
import { guestSessionPolicy, localSessionPolicy,
|
|
293
|
+
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
328
294
|
import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
|
|
329
|
-
import { DeviceLoginRequests } from "./web-device-login.js";
|
|
330
|
-
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";
|
|
331
295
|
import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
332
296
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
333
297
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
@@ -349,10 +313,9 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
349
313
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
350
314
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
351
315
|
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
352
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome,
|
|
316
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
|
|
353
317
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
354
|
-
import {
|
|
355
|
-
import { ACCOUNT_CLIS, PROFILE_CLIS, buildAccountsView, profilesOnDisk, weeklyUsage } from "./accounts-view.js";
|
|
318
|
+
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
356
319
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
357
320
|
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
358
321
|
import { FleetStartup } from "./fleet-startup.js";
|
|
@@ -805,1403 +768,7 @@ export class FleetManager {
|
|
|
805
768
|
/** Live view of lifecycle.daemons — used throughout; not deprecated. */
|
|
806
769
|
get daemons() { return this.lifecycle.daemons; }
|
|
807
770
|
fleetConfig = null;
|
|
808
|
-
fleetWindowsRuntime = null;
|
|
809
|
-
nativeMcpStore = null;
|
|
810
|
-
paneManager = null;
|
|
811
|
-
localPaneRelay = null;
|
|
812
|
-
openingLocalPane = null;
|
|
813
|
-
runtimeHub = null;
|
|
814
|
-
runtimeInstall;
|
|
815
|
-
/** One service for the public installer routes and the administrator API: the hash the dashboard shows is the hash of what is sent. */
|
|
816
|
-
runtimeInstaller() { return this.runtimeInstall ??= new RuntimeInstallService(); }
|
|
817
|
-
runtimeExecutor = null;
|
|
818
|
-
runtimeStatusScheduled = false;
|
|
819
|
-
reconciledRemoteResults = new Set();
|
|
820
|
-
/** Machine credentials are separate from dashboard sessions and per-turn agent capabilities. */
|
|
821
|
-
runtimeCoordinator() {
|
|
822
|
-
if (!remoteRuntimeEnabled())
|
|
823
|
-
throw new Error("Remote runtimes are not enabled");
|
|
824
|
-
return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
|
|
825
|
-
handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
|
|
826
|
-
handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
|
|
827
|
-
warn: (fields, message) => this.logger.warn(fields, message),
|
|
828
|
-
changed: event => {
|
|
829
|
-
if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
|
|
830
|
-
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
|
|
831
|
-
if (config.machine_id === event.machineId) {
|
|
832
|
-
this.nativeMcpStore?.closeInstance(name);
|
|
833
|
-
if (config.general_topic) {
|
|
834
|
-
this.cancelPendingDeliveries(name);
|
|
835
|
-
this.holdGeneralOldDeliveries(name);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
this.needsYou?.poke();
|
|
839
|
-
}
|
|
840
|
-
if (this.runtimeStatusScheduled || this.shuttingDown)
|
|
841
|
-
return;
|
|
842
|
-
this.runtimeStatusScheduled = true;
|
|
843
|
-
queueMicrotask(() => {
|
|
844
|
-
this.runtimeStatusScheduled = false;
|
|
845
|
-
if (this.shuttingDown)
|
|
846
|
-
return;
|
|
847
|
-
try {
|
|
848
|
-
this.fleetWindowsRuntime?.importHistory();
|
|
849
|
-
for (const snapshot of this.runtimeHub?.tasks() ?? [])
|
|
850
|
-
this.reconcileRemoteDelivery(snapshot.machineId, snapshot.task);
|
|
851
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
852
|
-
this.scheduleDeliveryOutboxPump();
|
|
853
|
-
}
|
|
854
|
-
catch (error) {
|
|
855
|
-
this.logger.warn({ err: error }, "Runtime status reconciliation failed");
|
|
856
|
-
}
|
|
857
|
-
});
|
|
858
|
-
},
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
runtimeWindowsExecutor() {
|
|
862
|
-
return this.runtimeExecutor ??= new RuntimeWindowsBridge(this, () => remoteRuntimeEnabled() && !this.shuttingDown ? this.runtimeCoordinator() : null);
|
|
863
|
-
}
|
|
864
|
-
/** Per-turn capabilities for native MCP, separate from browser and Linux-agent credentials. */
|
|
865
|
-
nativeMcpSessions() {
|
|
866
|
-
return this.nativeMcpStore ??= new NativeMcpSessions(this);
|
|
867
|
-
}
|
|
868
|
-
/** Retire only existing native capabilities; ordinary lifecycle changes do not instantiate the Windows path. */
|
|
869
|
-
invalidateNativeMcp(name) { this.nativeMcpStore?.closeInstance(name); }
|
|
870
|
-
/** The same instance registry and delivery entry point, with a native Windows execution adapter. */
|
|
871
|
-
nativeInstanceRuntime() {
|
|
872
|
-
return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
|
|
873
|
-
owner: this, fleet: () => this.fleetConfig, save: () => this.saveFleetConfig(), stopping: () => this.shuttingDown, history: this.webChatHistory,
|
|
874
|
-
openMcp: (instance, requestId, sandbox, stillCurrent) => this.healthServerListening
|
|
875
|
-
? this.nativeMcpSessions().grant({ instance, requestId, sandbox, stillCurrent, endpoint: `http://127.0.0.1:${this.fleetConfig?.health_port ?? 19280}/agent` }) : null,
|
|
876
|
-
warn: (message, error) => this.logger.warn({ err: error }, message),
|
|
877
|
-
removed: name => this.nativeInstanceRemoved(name),
|
|
878
|
-
paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
|
|
879
|
-
machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
|
|
880
|
-
machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
|
|
881
|
-
paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
|
|
882
|
-
validateMachine: (id, cli, sandbox, credentialProfile) => {
|
|
883
|
-
const machine = this.runtimeCoordinator().machine(id);
|
|
884
|
-
const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
|
|
885
|
-
if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
|
|
886
|
-
throw new Error("Choose an online runtime and an available CLI");
|
|
887
|
-
if (sandbox === "read-only" && !provider.readOnlySupport)
|
|
888
|
-
throw new Error("This runtime CLI cannot enforce read-only execution");
|
|
889
|
-
if (credentialProfile !== undefined) {
|
|
890
|
-
// Fail closed at selection time, not at the first message: an old Runtime would ignore the name and run another account.
|
|
891
|
-
if (!machine.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
|
|
892
|
-
throw new Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
|
|
893
|
-
const row = machine.inventory?.credentialProfiles?.profiles.find(profile => profile.name === credentialProfile);
|
|
894
|
-
if (!row)
|
|
895
|
-
throw new Error(`This computer has not reported a credential profile named "${credentialProfile}". On that computer run: agend-runtime login claude-code ${credentialProfile}, then choose Detect again in that computer's CLI diagnostics (Settings → Runtime).`);
|
|
896
|
-
if (row.loggedIn === false)
|
|
897
|
-
throw new Error(`Credential profile "${credentialProfile}" is not signed in on that computer. Sign in there: agend-runtime login claude-code ${credentialProfile}`);
|
|
898
|
-
}
|
|
899
|
-
},
|
|
900
|
-
emit: (event, data) => {
|
|
901
|
-
this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
|
|
902
|
-
if (event === "activity")
|
|
903
|
-
this.scheduleDeliveryOutboxPump();
|
|
904
|
-
},
|
|
905
|
-
}, { bridge: this.runtimeWindowsExecutor() });
|
|
906
|
-
}
|
|
907
|
-
agentRuntimeChangeService;
|
|
908
|
-
agentRuntimeChanges() {
|
|
909
|
-
if (this.agentRuntimeChangeService)
|
|
910
|
-
return this.agentRuntimeChangeService;
|
|
911
|
-
const windowPins = new Map();
|
|
912
|
-
return this.agentRuntimeChangeService ??= new AgentRuntimeChanges({
|
|
913
|
-
config: name => Object.hasOwn(this.fleetConfig?.instances ?? {}, name) ? this.fleetConfig.instances[name] : undefined,
|
|
914
|
-
defaults: () => this.fleetConfig?.defaults,
|
|
915
|
-
generation: () => [this.managerBootId, this.settingsGeneration],
|
|
916
|
-
epoch: name => this.lifecycle.epochOf(name),
|
|
917
|
-
backendState: (name, backend) => !isWindowsInstance(this.fleetConfig?.instances[name]) ? this.backendAvailabilityFor(name).find(row => row.backend === backend)?.state : undefined,
|
|
918
|
-
transition: (name, run) => this.lifecycle.runTransition(name, async (transition) => {
|
|
919
|
-
const config = this.fleetConfig?.instances[name];
|
|
920
|
-
if (!isWindowsInstance(config)) {
|
|
921
|
-
const path = join(this.getInstanceDir(name), "window-id");
|
|
922
|
-
let pin;
|
|
923
|
-
if (existsSync(path)) {
|
|
924
|
-
if (statSync(path).size > 128)
|
|
925
|
-
throw Error("Invalid saved CLI window identity");
|
|
926
|
-
pin = readFileSync(path, "utf8").trim();
|
|
927
|
-
if (!/^@\d+$/.test(pin))
|
|
928
|
-
throw Error("Invalid saved CLI window identity");
|
|
929
|
-
}
|
|
930
|
-
else if (this.daemons.has(name))
|
|
931
|
-
throw Error("The current CLI window identity is unavailable");
|
|
932
|
-
windowPins.set(name, pin);
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
return await run(async () => {
|
|
936
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
937
|
-
this.lifecycle.invalidate(name);
|
|
938
|
-
this.nativeInstanceRuntime().setPaused(name, true);
|
|
939
|
-
}
|
|
940
|
-
else
|
|
941
|
-
await this.stopInstance(name, transition);
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
finally {
|
|
945
|
-
windowPins.delete(name);
|
|
946
|
-
}
|
|
947
|
-
}),
|
|
948
|
-
idle: name => {
|
|
949
|
-
if (this.shuttingDown || this.lifecycle.hasWorkLease(name) || this.deliveryOutbox?.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending", "uncertain"]))
|
|
950
|
-
return false;
|
|
951
|
-
const config = this.fleetConfig?.instances[name];
|
|
952
|
-
if (!config)
|
|
953
|
-
return false;
|
|
954
|
-
if (isWindowsInstance(config)) {
|
|
955
|
-
try {
|
|
956
|
-
const agent = this.nativeInstanceRuntime().get(name);
|
|
957
|
-
if (!agent || agent.blocked || agent.activeId && !isNativePaneInstance(config))
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
catch {
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
if (!this.daemons.has(name) && !isWindowsInstance(config) && existsSync(join(this.getInstanceDir(name), "window-id")))
|
|
965
|
-
return false;
|
|
966
|
-
return !this.daemons.has(name) || this.getInstanceExecutionState(name) === "idle";
|
|
967
|
-
},
|
|
968
|
-
prepare: async (name, before, target, current) => {
|
|
969
|
-
if (!current())
|
|
970
|
-
throw Error("Runtime change authority expired");
|
|
971
|
-
if (isWindowsInstance(before))
|
|
972
|
-
this.nativeInstanceRuntime().importHistory(name);
|
|
973
|
-
if (target.runtime === "windows")
|
|
974
|
-
return this.nativeInstanceRuntime().prepareRuntimeChange(name, before, target, current);
|
|
975
|
-
const sameAccount = !isWindowsInstance(before) && target.backend === (before.backend || this.fleetConfig?.defaults?.backend || "claude-code");
|
|
976
|
-
const availability = (sameAccount ? this.backendAvailabilityFor(name) : this.backendAvailabilityRows()).find(row => row.backend === target.backend);
|
|
977
|
-
if (availability && ["not_installed", "not_signed_in", "too_old"].includes(availability.state))
|
|
978
|
-
throw Error("Choose an available CLI on the Fleet host");
|
|
979
|
-
const supplied = target.directory.trim(), cwd = supplied || join(this.dataDir, "workspaces", name);
|
|
980
|
-
if (!isAbsolute(cwd) || /^[A-Za-z]:[\\/]/.test(cwd) || /[\x00-\x1f]/.test(cwd))
|
|
981
|
-
throw Error("Enter an absolute Linux working directory or leave it empty");
|
|
982
|
-
if (!current())
|
|
983
|
-
throw Error("Runtime change authority expired");
|
|
984
|
-
if (supplied) {
|
|
985
|
-
if (!statSync(cwd).isDirectory())
|
|
986
|
-
throw Error("The Linux working directory is not a directory");
|
|
987
|
-
}
|
|
988
|
-
else
|
|
989
|
-
mkdirSync(cwd, { recursive: true, mode: 0o700 });
|
|
990
|
-
const config = { ...before, runtime: "wsl", working_directory: cwd, backend: target.backend, model: target.model };
|
|
991
|
-
if (before.backend !== target.backend)
|
|
992
|
-
config.model_failover = [];
|
|
993
|
-
for (const key of ["machine_id", "native_agent_id", "native_adapter", "native_sandbox"])
|
|
994
|
-
delete config[key];
|
|
995
|
-
if (!sameAccount)
|
|
996
|
-
delete config.backend_options;
|
|
997
|
-
return { config, discard: () => { } };
|
|
998
|
-
},
|
|
999
|
-
closed: async (name, before) => {
|
|
1000
|
-
if (this.daemons.has(name))
|
|
1001
|
-
return false;
|
|
1002
|
-
if (isWindowsInstance(before))
|
|
1003
|
-
return !this.nativeInstanceRuntime().get(name)?.activeId;
|
|
1004
|
-
// An unreadable tmux state is not evidence that the old process exited.
|
|
1005
|
-
const session = getTmuxSession();
|
|
1006
|
-
if (!await TmuxManager.sessionExistsStrict(session))
|
|
1007
|
-
return true;
|
|
1008
|
-
const pin = windowPins.get(name);
|
|
1009
|
-
return !(await TmuxManager.listWindowsStrict(session)).some(window => window.name === name || pin && window.id === pin);
|
|
1010
|
-
},
|
|
1011
|
-
fresh: (name, requestId) => {
|
|
1012
|
-
const dir = this.getInstanceDir(name), moved = [];
|
|
1013
|
-
const restore = () => { for (const [source, saved] of moved.reverse())
|
|
1014
|
-
if (!existsSync(source) && existsSync(saved))
|
|
1015
|
-
renameSync(saved, source); };
|
|
1016
|
-
try {
|
|
1017
|
-
for (const file of ["session-id", "session-id.backend"]) {
|
|
1018
|
-
const source = join(dir, file), saved = join(dir, `${file}.runtime-${requestId}`);
|
|
1019
|
-
if (existsSync(source)) {
|
|
1020
|
-
if (existsSync(saved))
|
|
1021
|
-
throw Error("Session backup already exists");
|
|
1022
|
-
renameSync(source, saved);
|
|
1023
|
-
moved.push([source, saved]);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
return restore;
|
|
1027
|
-
}
|
|
1028
|
-
catch (error) {
|
|
1029
|
-
restore();
|
|
1030
|
-
throw error;
|
|
1031
|
-
}
|
|
1032
|
-
},
|
|
1033
|
-
save: (name, before, next) => {
|
|
1034
|
-
if (this.fleetConfig?.instances[name] !== before)
|
|
1035
|
-
throw Error("Agent settings changed before saving Runtime");
|
|
1036
|
-
// Mark the replacement stopped before committing its config. A marker failure must not leave a saved
|
|
1037
|
-
// Windows config referring to a profile the preparation rollback would then remove.
|
|
1038
|
-
writePausedMarker(this.getInstanceDir(name), Date.now(), "operator");
|
|
1039
|
-
const fleet = this.fleetConfig;
|
|
1040
|
-
fleet.instances[name] = next;
|
|
1041
|
-
try {
|
|
1042
|
-
this.saveFleetConfig();
|
|
1043
|
-
}
|
|
1044
|
-
catch (error) {
|
|
1045
|
-
fleet.instances[name] = before;
|
|
1046
|
-
throw error;
|
|
1047
|
-
}
|
|
1048
|
-
// A move deliberately leaves the replacement stopped; its next explicit Start opens the fresh CLI.
|
|
1049
|
-
},
|
|
1050
|
-
changed: name => { this.instanceStateCache.delete(name); this.instanceProcessStatus.delete(name); this.emitSseEvent("status", this.getUiStatus()); },
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
/** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
|
|
1054
|
-
async createNativePaneInstance(profile, channelId, authorized, execution) {
|
|
1055
|
-
const { createNativePane } = await import("./native-pane-create.js");
|
|
1056
|
-
return createNativePane({ fleet: () => this.fleetConfig, runtime: this.nativeInstanceRuntime(),
|
|
1057
|
-
topic: target => {
|
|
1058
|
-
const world = this.worlds.get(target.id), adapter = world?.adapter;
|
|
1059
|
-
if (!world || !adapter?.createTopic || !adapter.deleteTopic)
|
|
1060
|
-
throw Error("The approved connection cannot create and compensate a topic");
|
|
1061
|
-
const same = () => {
|
|
1062
|
-
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1063
|
-
const matches = channels.filter(row => (row.id ?? row.type) === target.id);
|
|
1064
|
-
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;
|
|
1065
|
-
};
|
|
1066
|
-
return { current: same, create: name => adapter.createTopic(name), remove: id => adapter.deleteTopic(id) };
|
|
1067
|
-
},
|
|
1068
|
-
epoch: name => this.lifecycle.epochOf(name),
|
|
1069
|
-
start: async (name, config, current) => {
|
|
1070
|
-
if (!current())
|
|
1071
|
-
throw Error("Native creation expired before start");
|
|
1072
|
-
await this.startInstance(name, config, true);
|
|
1073
|
-
if (!current())
|
|
1074
|
-
throw Error("Native creation expired during start");
|
|
1075
|
-
await this.connectIpcToInstance(name);
|
|
1076
|
-
if (!current())
|
|
1077
|
-
throw Error("Native creation expired during channel connection");
|
|
1078
|
-
},
|
|
1079
|
-
close: (name, config, epoch) => this.lifecycle.runTransition(name, async (transition) => {
|
|
1080
|
-
if (this.fleetConfig?.instances[name] !== config || this.lifecycle.epochOf(name) !== epoch)
|
|
1081
|
-
return false;
|
|
1082
|
-
await this.stopInstance(name, transition);
|
|
1083
|
-
return this.fleetConfig?.instances[name] === config && this.lifecycle.epochOf(name) === epoch + 1;
|
|
1084
|
-
}),
|
|
1085
|
-
routes: () => { if (this.fleetConfig)
|
|
1086
|
-
this.routing.rebuild(this.fleetConfig); },
|
|
1087
|
-
warn: error => this.logger.warn({ err: error }, "Native creation cleanup is held; retained identity and topic require attention"), }, profile, channelId, () => !this.shuttingDown && authorized(), execution);
|
|
1088
|
-
}
|
|
1089
|
-
/** Admin-only settings path: validate first, then close the exact old launch before changing cold fields. */
|
|
1090
|
-
async updateNativePaneConfig(name, patch, authorized, transaction) {
|
|
1091
|
-
return this.lifecycle.runTransition(name, async (transition) => {
|
|
1092
|
-
const before = this.fleetConfig?.instances[name];
|
|
1093
|
-
if (!isNativePaneInstance(before))
|
|
1094
|
-
throw Error("Persistent Windows instance not found");
|
|
1095
|
-
const runtime = this.nativeInstanceRuntime(), generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1096
|
-
let expectedGeneration = generation;
|
|
1097
|
-
const current = () => authorized() && !this.shuttingDown && this.fleetConfig?.instances[name] === before
|
|
1098
|
-
&& (this.explicitStopGeneration.get(name) ?? 0) === expectedGeneration
|
|
1099
|
-
&& (before.machine_id ? remoteRuntimeEnabled() : localWindowsCliEnabled());
|
|
1100
|
-
if (!current())
|
|
1101
|
-
throw Error("Native settings authority expired");
|
|
1102
|
-
runtime.previewUpdate(name, patch);
|
|
1103
|
-
const cold = ["working_directory", "model", "systemPrompt", "native_sandbox"].some(key => Object.hasOwn(patch, key) && (patch[key] ?? "") !== (before[key] ?? ""));
|
|
1104
|
-
const running = this.daemons.get(name);
|
|
1105
|
-
if (cold) {
|
|
1106
|
-
if (running && !running.nativePaneHasExited() && (!await running.nativePauseReady({ current, accepts: () => true, revalidate: async () => current() }) || !running.nativePauseStillReady()))
|
|
1107
|
-
throw Error("Wait for this Windows agent to be idle before changing its launch settings");
|
|
1108
|
-
if (!current())
|
|
1109
|
-
throw Error("Native settings authority expired before closing its old pane");
|
|
1110
|
-
await this.stopInstance(name, transition);
|
|
1111
|
-
expectedGeneration = generation + 1;
|
|
1112
|
-
if (!current())
|
|
1113
|
-
throw Error("Native settings authority expired after closing its old pane");
|
|
1114
|
-
if (runtime.get(name)?.activeId)
|
|
1115
|
-
throw Error("Native pane exit is unconfirmed; its settings were not changed");
|
|
1116
|
-
}
|
|
1117
|
-
const result = runtime.update(name, patch, current, transaction), next = this.fleetConfig.instances[name];
|
|
1118
|
-
if (cold && running) {
|
|
1119
|
-
if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== next || (this.explicitStopGeneration.get(name) ?? 0) !== expectedGeneration)
|
|
1120
|
-
throw Error("Native settings saved; restart authority expired");
|
|
1121
|
-
await this.startInstance(name, next, true, "fleet-topic", false, transition);
|
|
1122
|
-
}
|
|
1123
|
-
return result;
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
/** The local machine's inventory as last used to open its pane relay (status only; never a launch decision). */
|
|
1127
|
-
localPaneInventory = null;
|
|
1128
|
-
nativeCliStatus(cli, machineId, daemon) {
|
|
1129
|
-
if (cli !== "codex" && cli !== "claude-code")
|
|
1130
|
-
return {};
|
|
1131
|
-
// The version the running pane actually launched; only without one, the machine's last inventory.
|
|
1132
|
-
let running;
|
|
1133
|
-
try {
|
|
1134
|
-
running = daemon?.usesNativePane?.() ? daemon.nativePaneCliVersion() : undefined;
|
|
1135
|
-
}
|
|
1136
|
-
catch {
|
|
1137
|
-
running = undefined;
|
|
1138
|
-
}
|
|
1139
|
-
const inventory = machineId ? this.runtimeHub?.machine(machineId)?.inventory : this.localPaneInventory;
|
|
1140
|
-
const version = running ?? inventory?.clis?.find(item => item.id === cli && item.runnable)?.version;
|
|
1141
|
-
if (version === undefined || version === null)
|
|
1142
|
-
return {};
|
|
1143
|
-
const result = nativeCliVersionState(cli, version);
|
|
1144
|
-
return { ...(result.version ? { native_cli_version: result.version } : {}), native_cli_verification: result.state };
|
|
1145
|
-
}
|
|
1146
|
-
async localNativePaneEndpoint() {
|
|
1147
|
-
const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
|
|
1148
|
-
throw Error("Local Windows runtime is switched off or draining"); };
|
|
1149
|
-
admit();
|
|
1150
|
-
if (this.localPaneDrain)
|
|
1151
|
-
throw Error("Local Windows runtime cleanup is pending");
|
|
1152
|
-
if (this.localPaneRelay?.online())
|
|
1153
|
-
return this.localPaneRelay;
|
|
1154
|
-
if (this.openingLocalPane)
|
|
1155
|
-
return this.openingLocalPane;
|
|
1156
|
-
const opening = (async () => {
|
|
1157
|
-
const [{ LocalPaneEndpoint }, { launchLocalPaneRelay, localPaneScope }, inventory] = await Promise.all([
|
|
1158
|
-
import("./local-pane-endpoint.js"), import("./local-pane-launcher.js"), this.runtimeWindowsExecutor().probe({ force: false }),
|
|
1159
|
-
]);
|
|
1160
|
-
admit();
|
|
1161
|
-
this.localPaneRelay?.stop();
|
|
1162
|
-
this.localPaneInventory = inventory;
|
|
1163
|
-
const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
|
|
1164
|
-
diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
|
|
1165
|
-
version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
|
|
1166
|
-
launch: () => launchLocalPaneRelay(this.dataDir, admit),
|
|
1167
|
-
mcp: async (body, current) => (await this.nativePanes()).handle("local", body, current),
|
|
1168
|
-
});
|
|
1169
|
-
try {
|
|
1170
|
-
admit();
|
|
1171
|
-
this.localPaneRelay = relay;
|
|
1172
|
-
return relay;
|
|
1173
|
-
}
|
|
1174
|
-
catch (error) {
|
|
1175
|
-
relay.stop();
|
|
1176
|
-
throw error;
|
|
1177
|
-
}
|
|
1178
|
-
})();
|
|
1179
|
-
this.openingLocalPane = opening;
|
|
1180
|
-
try {
|
|
1181
|
-
return await opening;
|
|
1182
|
-
}
|
|
1183
|
-
finally {
|
|
1184
|
-
if (this.openingLocalPane === opening)
|
|
1185
|
-
this.openingLocalPane = null;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
/** Persistent ownership is independent of the exec worker's per-turn capabilities. */
|
|
1189
|
-
async nativePanes() {
|
|
1190
|
-
if (this.paneManager)
|
|
1191
|
-
return this.paneManager;
|
|
1192
|
-
const { NativePaneManager } = await import("./native-pane-manager.js");
|
|
1193
|
-
if (this.paneManager)
|
|
1194
|
-
return this.paneManager;
|
|
1195
|
-
const store = windowsAgentsFor(this, this.runtimeWindowsExecutor());
|
|
1196
|
-
const allowed = (name, tool) => nativePaneToolAvailable(tool) && mayUseTool(resolveToolSet(this.fleetConfig?.instances[name], name), tool)
|
|
1197
|
-
&& !dashboardRefusalIn(this.fleetConfig, name, tool);
|
|
1198
|
-
return this.paneManager = new NativePaneManager({
|
|
1199
|
-
home: this.dataDir, store, config: name => this.fleetConfig?.instances[name],
|
|
1200
|
-
freshNotice: async (name, provider) => {
|
|
1201
|
-
const message = t(provider === "codex" ? "native_pane.codex_fresh_started" : "native_pane.fresh_started");
|
|
1202
|
-
this.eventLog?.insert(name, "native_pane_fresh_started", {});
|
|
1203
|
-
this.logger.info({ name }, message);
|
|
1204
|
-
await this.notifyInstanceTopic(name, message);
|
|
1205
|
-
},
|
|
1206
|
-
admit: (name, config) => {
|
|
1207
|
-
const now = this.fleetConfig?.instances[name];
|
|
1208
|
-
if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
|
|
1209
|
-
|| now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
|
|
1210
|
-
|| now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
|
|
1211
|
-
|| now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding
|
|
1212
|
-
|| nativeCredentialProfile(now) !== nativeCredentialProfile(config))
|
|
1213
|
-
throw Error("Native pane configuration or lifetime changed");
|
|
1214
|
-
// Fail closed: a Runtime that does not report the capability would start the terminal on the computer's own account.
|
|
1215
|
-
if (nativeCredentialProfile(config) && !this.runtimeCoordinator().machine(config.machine_id)?.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
|
|
1216
|
-
throw Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
|
|
1217
|
-
if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
|
|
1218
|
-
throw Error("Native runtime is switched off");
|
|
1219
|
-
},
|
|
1220
|
-
endpoint: async (agent, cleanup = false) => {
|
|
1221
|
-
if (!agent.machineId) {
|
|
1222
|
-
if (!cleanup)
|
|
1223
|
-
return this.localNativePaneEndpoint();
|
|
1224
|
-
if (!this.localPaneRelay)
|
|
1225
|
-
throw Error("Local cleanup transport is unavailable; pane remains reserved");
|
|
1226
|
-
return this.localPaneRelay;
|
|
1227
|
-
}
|
|
1228
|
-
const hub = cleanup ? this.runtimeHub : this.runtimeCoordinator(), machineId = agent.machineId;
|
|
1229
|
-
if (!hub)
|
|
1230
|
-
throw Error("Remote cleanup transport is unavailable; pane remains reserved");
|
|
1231
|
-
const draining = () => !!this.remotePaneDrain;
|
|
1232
|
-
return { machineId, online: () => !this.shuttingDown && (draining() || remoteRuntimeEnabled()) && this.runtimeHub === hub && hub.machine(machineId)?.status === "online",
|
|
1233
|
-
call: request => {
|
|
1234
|
-
if (this.runtimeHub !== hub || !(draining() || remoteRuntimeEnabled()) || draining() && request.action !== "snapshot" && request.action !== "close")
|
|
1235
|
-
return Promise.reject(Error("Remote pane transport only permits owned cleanup"));
|
|
1236
|
-
return hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1237
|
-
}, subscribe: listener => hub.subscribe(listener),
|
|
1238
|
-
epoch: () => hub.paneTransportEpoch(machineId), version: provider => hub.machine(machineId)?.inventory?.clis.find(row => row.id === provider && row.runnable)?.version ?? null };
|
|
1239
|
-
},
|
|
1240
|
-
instructions: (name, config) => buildNativeFleetInstructions({ instanceName: name, workingDirectory: config.working_directory,
|
|
1241
|
-
displayName: config.display_name, description: config.description, customPrompt: config.systemPrompt,
|
|
1242
|
-
workflow: config.workflow, taskBoard: !dashboardRefusalIn(this.fleetConfig, name, "task"),
|
|
1243
|
-
runtimeIdentity: { kind: "fleet-topic", backend: config.backend ?? "codex", model: config.model || "default" } }, config.general_topic === true),
|
|
1244
|
-
menu: name => TOOLS.filter(tool => allowed(name, tool.name)).map(tool => ({ ...tool })),
|
|
1245
|
-
allowed: (name, tool, args) => allowed(name, tool) && !nativePaneToolRefusal(tool, args), published: (name, daemon) => this.daemons.get(name) === daemon,
|
|
1246
|
-
invoke: async (daemon, input, current, name) => {
|
|
1247
|
-
if (input.tool === "list_models") {
|
|
1248
|
-
const { ListModelsArgs } = await import("./outbound-schemas.js");
|
|
1249
|
-
const parsed = ListModelsArgs.safeParse(input.args);
|
|
1250
|
-
if (!parsed.success || !current())
|
|
1251
|
-
return { result: null, error: "Invalid native model request or expired owner" };
|
|
1252
|
-
const target = parsed.data.instance_name ?? name;
|
|
1253
|
-
if (!isWindowsInstance(this.fleetConfig?.instances[target]))
|
|
1254
|
-
return { result: null, error: "Native panes can only read Windows runtime model inventories" };
|
|
1255
|
-
return { result: await this.listModelCatalog({ instanceName: target }) };
|
|
1256
|
-
}
|
|
1257
|
-
const ops = { list_instances: "list", describe_instance: "describe", task: "task", send_to_instance: "send",
|
|
1258
|
-
delegate_task: "delegate", request_information: "ask", report_result: "report", delivery_status: "delivery-status" };
|
|
1259
|
-
const { nativeAgentAdmissionContext } = await import("./native-agent-admission.js");
|
|
1260
|
-
const { dispatchAgentOperation } = await import("./agent-endpoint.js");
|
|
1261
|
-
return daemon.handlePaneToolCall(input, { current, allowed: tool => allowed(name, tool),
|
|
1262
|
-
dispatch: (tool, args) => ops[tool] ? dispatchAgentOperation(nativeAgentAdmissionContext(this, {
|
|
1263
|
-
instance: name, current: () => current() && allowed(name, tool), taskAllowed: () => current() && allowed(name, "task"),
|
|
1264
|
-
}), name, ops[tool], args, input.operationId) : undefined });
|
|
1265
|
-
},
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
/** Prepare only bundled role files; this neither switches General nor starts a provider process. */
|
|
1269
|
-
async prepareWindowsGeneralWorkspace(name, machineId, cli, bindingId, authorized) {
|
|
1270
|
-
const config = this.fleetConfig?.instances[name], hub = this.runtimeCoordinator(), epoch = hub.paneTransportEpoch(machineId);
|
|
1271
|
-
if (!config?.general_topic || !epoch)
|
|
1272
|
-
throw Error("General or its paired runtime is unavailable");
|
|
1273
|
-
const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
|
|
1274
|
-
&& this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
|
|
1275
|
-
const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertRunnableNativePaneVersion }] = await Promise.all([
|
|
1276
|
-
import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
|
|
1277
|
-
]);
|
|
1278
|
-
if (!current())
|
|
1279
|
-
throw Error("General workspace authority expired");
|
|
1280
|
-
assertRunnableNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
|
|
1281
|
-
const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
|
|
1282
|
-
const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
|
|
1283
|
-
if (!current())
|
|
1284
|
-
throw Error("General workspace authority expired");
|
|
1285
|
-
if (inventory?.capabilities?.generalWorkspace !== true)
|
|
1286
|
-
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1287
|
-
const result = await hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1288
|
-
if (!current())
|
|
1289
|
-
throw Error("General workspace authority expired");
|
|
1290
|
-
if (generalWorkspaceUpgrade(result, request))
|
|
1291
|
-
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1292
|
-
return validateGeneralWorkspaceReceipt(result, request);
|
|
1293
|
-
}
|
|
1294
|
-
/** S3c (S-55): the member's exposure API, attached by S5's member composition (null: no Hub member here). */
|
|
1295
|
-
hubMemberExposureApi = null;
|
|
1296
|
-
hubMemberExposures() { return this.hubMemberExposureApi; }
|
|
1297
|
-
attachHubMemberExposures(api) { this.hubMemberExposureApi = api; }
|
|
1298
|
-
/** S3c PR B: the member's pairing API, attached by S5's member composition (null: no Hub member here). */
|
|
1299
|
-
hubMemberPairingApi = null;
|
|
1300
|
-
hubMemberPairing() { return this.hubMemberPairingApi; }
|
|
1301
|
-
attachHubMemberPairing(api) { this.hubMemberPairingApi = api; }
|
|
1302
|
-
generalLocationFlights = new Map();
|
|
1303
|
-
generalAttentionEpisodes = new Map();
|
|
1304
|
-
generalBindings() { return new GeneralBindings(this.dataDir); }
|
|
1305
|
-
generalLocations() {
|
|
1306
|
-
const store = this.generalBindings();
|
|
1307
|
-
return { generals: Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => cfg.general_topic).map(([name, cfg]) => {
|
|
1308
|
-
const record = store.read(name), machine = cfg.machine_id ? this.runtimeHub?.machine(cfg.machine_id) : null;
|
|
1309
|
-
return { name, displayName: cfg.display_name ?? name, location: isWindowsInstance(cfg) ? "windows" : "host",
|
|
1310
|
-
machineId: cfg.machine_id ?? null, cli: cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code", model: cfg.model ?? "",
|
|
1311
|
-
revision: generalBindingRevision(record, cfg), canSwitchBack: !!record && record.active !== "host",
|
|
1312
|
-
offline: isWindowsInstance(cfg) && (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(cfg.machine_id)),
|
|
1313
|
-
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,
|
|
1314
|
-
hold: record?.hold ?? (record?.pending ? record.pending.phase === "selected" ? "start" : "exit" : null), switching: this.generalLocationFlights.has(name) };
|
|
1315
|
-
}), machines: (this.runtimeHub?.listMachines() ?? []).filter(row => row.status !== "pending" && row.status !== "revoked").map(row => ({
|
|
1316
|
-
id: row.id, name: row.name, online: row.status === "online" && !!this.runtimeHub?.paneTransportEpoch(row.id),
|
|
1317
|
-
clis: row.inventory?.clis.filter(cli => cli.runnable && (cli.id === "claude-code" || cli.id === "codex")).map(cli => ({ id: cli.id, version: cli.version })) ?? [],
|
|
1318
|
-
})) };
|
|
1319
|
-
}
|
|
1320
|
-
/** Pure preflight for the confirmation diff and stale-tab refusal; never prepares files or starts a CLI. */
|
|
1321
|
-
previewGeneralLocation(raw) {
|
|
1322
|
-
const input = parseGeneralSwitchInput(raw), cfg = this.fleetConfig?.instances[input.name];
|
|
1323
|
-
if (!cfg?.general_topic || this.shuttingDown)
|
|
1324
|
-
throw Error("General is unavailable");
|
|
1325
|
-
const record = this.generalBindings().read(input.name);
|
|
1326
|
-
if (input.revision !== generalBindingRevision(record, cfg))
|
|
1327
|
-
throw Error("General location changed; read its settings again");
|
|
1328
|
-
const connections = cfg.channel_id ? [cfg.channel_id] : [];
|
|
1329
|
-
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1330
|
-
if (channels.length && (connections.length !== 1 || channels.filter(row => (row.id ?? row.type) === connections[0]).length !== 1))
|
|
1331
|
-
throw Error("General's connection cannot be resolved");
|
|
1332
|
-
if (input.target.location === "host") {
|
|
1333
|
-
if (!record)
|
|
1334
|
-
throw Error("General's saved host location is unavailable");
|
|
1335
|
-
return { input, connections, summary: [t("general_location.confirm_host"), t("general_location.history")] };
|
|
1336
|
-
}
|
|
1337
|
-
const active = record?.windows.find(row => row.id === record.active);
|
|
1338
|
-
if (!record?.pending && record?.hold !== "start" && active?.machineId === input.target.machineId && active.cli === input.target.cli)
|
|
1339
|
-
throw Error("General already runs at this location");
|
|
1340
|
-
const machine = this.runtimeHub?.machine(input.target.machineId);
|
|
1341
|
-
if (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(machine.id))
|
|
1342
|
-
throw Error(t("general_location.offline"));
|
|
1343
|
-
const provider = input.target.cli;
|
|
1344
|
-
if (!machine.inventory?.clis.some(cli => cli.id === provider && cli.runnable))
|
|
1345
|
-
throw Error("The chosen Windows CLI is unavailable");
|
|
1346
|
-
return { input, connections, summary: [t("general_location.confirm_windows", settingsDisplay(machine.name)), t("general_location.history")] };
|
|
1347
|
-
}
|
|
1348
|
-
/** All entry points, including automatic native recovery, must respect an interrupted binding switch. */
|
|
1349
|
-
assertGeneralBindingStart(name, config) {
|
|
1350
|
-
if (!config.general_topic)
|
|
1351
|
-
return;
|
|
1352
|
-
try {
|
|
1353
|
-
this.generalBindings().assertStart(name, config);
|
|
1354
|
-
}
|
|
1355
|
-
catch (err) {
|
|
1356
|
-
this.logger.warn({ name, err }, "General location start held by its saved binding");
|
|
1357
|
-
this.needsYou?.poke();
|
|
1358
|
-
throw new UnsupportedCliError(t("general_location.exit_unconfirmed"));
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
/** Only the Settings replay's confirmed admin can enter; the whole switch owns one lifecycle transition. */
|
|
1362
|
-
async switchGeneralLocation(raw, authorized, execution) {
|
|
1363
|
-
const { input } = this.previewGeneralLocation(raw), name = input.name;
|
|
1364
|
-
return this.lifecycle.runTransition(name, async (transition) => {
|
|
1365
|
-
execution.assert();
|
|
1366
|
-
if (!authorized() || this.generalLocationFlights.has(name))
|
|
1367
|
-
throw Error("General location authority expired or a switch is already running");
|
|
1368
|
-
const token = Symbol(name), generation = this.settingsGeneration;
|
|
1369
|
-
const destinationEpoch = input.target.location === "windows" ? this.runtimeHub?.paneTransportEpoch(input.target.machineId) : null;
|
|
1370
|
-
let expected = this.fleetConfig.instances[name], stopGeneration = this.explicitStopGeneration.get(name) ?? 0, epoch = this.lifecycle.epochOf(name);
|
|
1371
|
-
const hostSnapshot = !isWindowsInstance(expected) ? structuredClone(this.daemons.get(name)?.getConfigSnapshot() ?? expected) : undefined;
|
|
1372
|
-
const current = () => !this.shuttingDown && this.settingsGeneration === generation && authorized()
|
|
1373
|
-
&& this.generalLocationFlights.get(name) === token && this.fleetConfig?.instances[name] === expected
|
|
1374
|
-
&& (input.target.location !== "windows" || remoteRuntimeEnabled() && !!destinationEpoch && this.runtimeHub?.paneTransportEpoch(input.target.machineId) === destinationEpoch)
|
|
1375
|
-
&& (this.explicitStopGeneration.get(name) ?? 0) === stopGeneration && this.lifecycle.epochOf(name) === epoch;
|
|
1376
|
-
const check = () => { if (!current())
|
|
1377
|
-
throw Error("General location switch was superseded"); };
|
|
1378
|
-
this.generalLocationFlights.set(name, token);
|
|
1379
|
-
// Old ephemeral waiters carry this epoch; they cannot wake into the selected conversation.
|
|
1380
|
-
this.cancelPendingDeliveries(name);
|
|
1381
|
-
let lease = null;
|
|
1382
|
-
try {
|
|
1383
|
-
lease = await waitSettingsLease([settingsFileResource(this.getSettingsConfigPath()), `general:${this.dataDir}:${name}`], () => current() && execution.current(), performance.now() + execution.remainingMs, execution.owner);
|
|
1384
|
-
const store = this.generalBindings();
|
|
1385
|
-
const result = await switchGeneralLocation({ store, current: () => expected, assert: check,
|
|
1386
|
-
prepare: async (target, retained) => {
|
|
1387
|
-
if (target.location !== "windows")
|
|
1388
|
-
return null;
|
|
1389
|
-
const old = retained.windows.find(row => row.machineId === target.machineId && row.cli === target.cli);
|
|
1390
|
-
const id = old?.id ?? randomUUID(), agentId = old?.agentId ?? randomUUID();
|
|
1391
|
-
const publication = await this.prepareWindowsGeneralWorkspace(name, target.machineId, target.cli, id, () => current() && execution.current());
|
|
1392
|
-
check();
|
|
1393
|
-
execution.assert();
|
|
1394
|
-
const config = { ...structuredClone(expected), runtime: "windows", machine_id: target.machineId,
|
|
1395
|
-
native_adapter: "pane", native_agent_id: agentId, native_general_binding: id, native_sandbox: "workspace-write",
|
|
1396
|
-
backend: target.cli, model: target.model, working_directory: publication.cwd, systemPrompt: "", agent_mode: "cli" };
|
|
1397
|
-
// Host credentials, shell hooks and provider-specific options never become Windows launch configuration.
|
|
1398
|
-
for (const key of ["backend_options", "credential_profile", "pre_task_command"])
|
|
1399
|
-
delete config[key];
|
|
1400
|
-
windowsAgentsFor(this, this.runtimeWindowsExecutor()).ensureGeneralBinding({ id: agentId, instance: name, machineId: target.machineId,
|
|
1401
|
-
cli: target.cli, cwd: publication.cwd, name: (expected.display_name ?? name).slice(0, 64), model: target.model, instructions: "", description: expected.description ?? "" }, () => current() && execution.current());
|
|
1402
|
-
return { id, agentId, machineId: target.machineId, cli: target.cli, config };
|
|
1403
|
-
},
|
|
1404
|
-
idle: async () => {
|
|
1405
|
-
check();
|
|
1406
|
-
const daemon = this.daemons.get(name);
|
|
1407
|
-
return !daemon || daemon.getInstanceStateSnapshot().state === "idle" && daemon.getWorkingRecoveryState().idle
|
|
1408
|
-
&& daemon.getInteractionSnapshot().phase === "clear" && !this.lifecycle.hasWorkLease(name)
|
|
1409
|
-
&& Date.now() - (this.lastDeliveryAt.get(name) ?? 0) >= 2000;
|
|
1410
|
-
},
|
|
1411
|
-
hostSnapshot: () => hostSnapshot,
|
|
1412
|
-
capture: async () => {
|
|
1413
|
-
if (isWindowsInstance(expected))
|
|
1414
|
-
return undefined;
|
|
1415
|
-
const daemon = this.daemons.get(name);
|
|
1416
|
-
if (!daemon)
|
|
1417
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1418
|
-
const owner = await daemon.generalHostOwner();
|
|
1419
|
-
check();
|
|
1420
|
-
execution.assert();
|
|
1421
|
-
return owner;
|
|
1422
|
-
},
|
|
1423
|
-
stop: async (owner) => {
|
|
1424
|
-
check();
|
|
1425
|
-
const stopping = this.stopInstance(name, transition);
|
|
1426
|
-
stopGeneration++;
|
|
1427
|
-
epoch++;
|
|
1428
|
-
await stopping;
|
|
1429
|
-
check();
|
|
1430
|
-
if (isWindowsInstance(expected)) {
|
|
1431
|
-
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(expected.native_agent_id);
|
|
1432
|
-
if (profile.activeId || profile.paneLaunchId)
|
|
1433
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1434
|
-
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1435
|
-
return undefined;
|
|
1436
|
-
}
|
|
1437
|
-
if (!owner || !await confirmGeneralHostExit(owner, getTmuxSession()))
|
|
1438
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1439
|
-
check();
|
|
1440
|
-
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1441
|
-
return owner.pin;
|
|
1442
|
-
},
|
|
1443
|
-
save: next => {
|
|
1444
|
-
check();
|
|
1445
|
-
const previous = expected;
|
|
1446
|
-
try {
|
|
1447
|
-
this.fleetConfig.instances[name] = next;
|
|
1448
|
-
this.saveFleetConfig([{ path: ["instances", name], value: next }]);
|
|
1449
|
-
expected = next;
|
|
1450
|
-
this.routing.rebuild(this.fleetConfig);
|
|
1451
|
-
}
|
|
1452
|
-
catch (err) {
|
|
1453
|
-
this.fleetConfig.instances[name] = previous;
|
|
1454
|
-
throw err;
|
|
1455
|
-
}
|
|
1456
|
-
},
|
|
1457
|
-
start: async () => {
|
|
1458
|
-
check();
|
|
1459
|
-
if (!execution.completed)
|
|
1460
|
-
throw Error("General's selected binding has no committed approval");
|
|
1461
|
-
// The committed receipt permits only this selected startup. Live authority still fences every awaited spawn boundary.
|
|
1462
|
-
const startup = new SettingsExecution({ current: () => current() && execution.completed && execution.remainingMs > 0,
|
|
1463
|
-
snapshot: () => this.fleetConfig });
|
|
1464
|
-
try {
|
|
1465
|
-
await this.startInstance(name, expected, true, "fleet-topic", false, transition, startup);
|
|
1466
|
-
}
|
|
1467
|
-
finally {
|
|
1468
|
-
startup.close();
|
|
1469
|
-
}
|
|
1470
|
-
check();
|
|
1471
|
-
const daemon = this.daemons.get(name);
|
|
1472
|
-
if (!daemon || daemon.getProcessStatus() !== "running" || isNativePaneInstance(expected) && !daemon.nativePaneCurrent())
|
|
1473
|
-
throw Error(t("general_location.start_failed"));
|
|
1474
|
-
await this.connectIpcToInstance(name);
|
|
1475
|
-
check();
|
|
1476
|
-
},
|
|
1477
|
-
notify: (kind, err) => {
|
|
1478
|
-
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"));
|
|
1479
|
-
this.eventLog?.insert(name, "general_location", { phase: kind, message });
|
|
1480
|
-
if (err)
|
|
1481
|
-
this.logger.warn({ name, phase: kind, err }, message);
|
|
1482
|
-
else
|
|
1483
|
-
this.logger.info({ name, phase: kind }, message);
|
|
1484
|
-
this.needsYou?.poke();
|
|
1485
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1486
|
-
this.notifyInstanceTopic(name, message);
|
|
1487
|
-
},
|
|
1488
|
-
}, input, execution);
|
|
1489
|
-
return { ...result, message: result.started ? t("general_location.complete", isWindowsInstance(expected)
|
|
1490
|
-
? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host")) : t("general_location.start_failed") };
|
|
1491
|
-
}
|
|
1492
|
-
finally {
|
|
1493
|
-
lease?.release();
|
|
1494
|
-
if (this.generalLocationFlights.get(name) === token)
|
|
1495
|
-
this.generalLocationFlights.delete(name);
|
|
1496
|
-
this.needsYou?.poke();
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
/** An offline PC or a confirmed binding boundary holds old waiting input; it never becomes another conversation's queue. */
|
|
1501
|
-
holdGeneralOldDeliveries(name) {
|
|
1502
|
-
const outbox = this.deliveryOutbox;
|
|
1503
|
-
if (!outbox)
|
|
1504
|
-
return;
|
|
1505
|
-
for (const row of outbox.listPending().filter(row => row.targetInstance === name)) {
|
|
1506
|
-
if (row.state !== "submission_started")
|
|
1507
|
-
outbox.failBeforeBegin(row.deliveryId, t("general_location.input_not_replayed"));
|
|
1508
|
-
else if (row.targetDaemonBootId) {
|
|
1509
|
-
outbox.markTargetReconciliationPending(name, row.targetDaemonBootId);
|
|
1510
|
-
if (!outbox.reconcileAttempt(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "uncertain", t("general_location.input_not_replayed")))
|
|
1511
|
-
throw Error("General's old delivery could not be held");
|
|
1512
|
-
}
|
|
1513
|
-
else
|
|
1514
|
-
throw Error("General's old delivery has no confirmed owner");
|
|
1515
|
-
}
|
|
1516
|
-
this.needsYou?.poke();
|
|
1517
|
-
}
|
|
1518
|
-
generalLocationDeliveryHold(name) {
|
|
1519
|
-
const config = this.fleetConfig?.instances[name];
|
|
1520
|
-
if (!config?.general_topic)
|
|
1521
|
-
return null;
|
|
1522
|
-
if (this.generalLocationFlights?.has(name))
|
|
1523
|
-
return t("general_location.exit_unconfirmed");
|
|
1524
|
-
let record;
|
|
1525
|
-
try {
|
|
1526
|
-
record = this.generalBindings().read(name);
|
|
1527
|
-
}
|
|
1528
|
-
catch {
|
|
1529
|
-
return t("general_location.start_failed");
|
|
1530
|
-
}
|
|
1531
|
-
if (record?.pending || record?.hold)
|
|
1532
|
-
return t(record.pending?.phase === "selected" || record.hold === "start" ? "general_location.start_failed" : "general_location.exit_unconfirmed");
|
|
1533
|
-
if (isWindowsInstance(config) && config.machine_id && (!remoteRuntimeEnabled() || this.runtimeHub?.machine(config.machine_id)?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(config.machine_id)))
|
|
1534
|
-
return t("general_location.offline");
|
|
1535
|
-
return null;
|
|
1536
|
-
}
|
|
1537
|
-
generalLocationAttention(name) {
|
|
1538
|
-
const cfg = this.fleetConfig?.instances[name];
|
|
1539
|
-
if (!cfg?.general_topic)
|
|
1540
|
-
return null;
|
|
1541
|
-
const detail = this.generalLocationDeliveryHold(name);
|
|
1542
|
-
if (!detail) {
|
|
1543
|
-
this.generalAttentionEpisodes.delete(name);
|
|
1544
|
-
return null;
|
|
1545
|
-
}
|
|
1546
|
-
let record = null;
|
|
1547
|
-
try {
|
|
1548
|
-
record = this.generalBindings().read(name);
|
|
1549
|
-
}
|
|
1550
|
-
catch { /* a corrupt journal is a visible hold too */ }
|
|
1551
|
-
const offline = !record?.pending && !record?.hold && isWindowsInstance(cfg) && detail === t("general_location.offline");
|
|
1552
|
-
const key = record?.pending?.id ?? `${record?.revision ?? "unknown"}:${offline ? "offline" : "hold"}`;
|
|
1553
|
-
let episode = this.generalAttentionEpisodes.get(name);
|
|
1554
|
-
if (!episode || episode.key !== key) {
|
|
1555
|
-
episode = { key, since: record?.pending?.at ?? Date.now() };
|
|
1556
|
-
this.generalAttentionEpisodes.set(name, episode);
|
|
1557
|
-
}
|
|
1558
|
-
return { ...episode, detail, offline };
|
|
1559
|
-
}
|
|
1560
|
-
async nativePaneFor(name, config, assertCurrent) {
|
|
1561
|
-
try {
|
|
1562
|
-
return await (await this.nativePanes()).prepare(name, config, assertCurrent);
|
|
1563
|
-
}
|
|
1564
|
-
catch (err) {
|
|
1565
|
-
if (config.general_topic && err instanceof GeneralManagedFileError) {
|
|
1566
|
-
assertCurrent();
|
|
1567
|
-
const store = this.generalBindings(), record = store.read(name);
|
|
1568
|
-
if (record && record.active === config.native_general_binding) {
|
|
1569
|
-
record.hold = "start";
|
|
1570
|
-
store.write(record);
|
|
1571
|
-
}
|
|
1572
|
-
this.logger.warn({ name, managedFile: err.file }, t("general_location.start_failed"));
|
|
1573
|
-
this.needsYou?.poke();
|
|
1574
|
-
throw new UnsupportedCliError(t("general_location.start_failed"));
|
|
1575
|
-
}
|
|
1576
|
-
throw err;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
async recoverNativePane(name, launchId, authorized) {
|
|
1580
|
-
const config = this.fleetConfig?.instances[name];
|
|
1581
|
-
if (!isNativePaneInstance(config))
|
|
1582
|
-
throw Error("Persistent Windows pane is not configured");
|
|
1583
|
-
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1584
|
-
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1585
|
-
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1586
|
-
throw Error(t("native_pane.authority_expired")); };
|
|
1587
|
-
current();
|
|
1588
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1589
|
-
throw Error(t("native_pane.still_running"));
|
|
1590
|
-
this.cancelStartupRetry(name);
|
|
1591
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1592
|
-
await (await this.nativePanes()).recover(name, config, launchId, true, current);
|
|
1593
|
-
current();
|
|
1594
|
-
// The host supplied a positive exit before ordinary lifecycle cleanup. No signal or input is required.
|
|
1595
|
-
await this.lifecycle.stop(name, transition);
|
|
1596
|
-
current();
|
|
1597
|
-
this.instanceProcessStatus.set(name, "stopped");
|
|
1598
|
-
this.eventLog?.insert(name, "native_pane_recovered", { launchId, confirmedStopped: true });
|
|
1599
|
-
this.logger.info({ name, launchId }, "Windows saved pane recovered by an administrator; waiting for Start");
|
|
1600
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
holdNativePaneRecovery(name, error) {
|
|
1604
|
-
if (!(error instanceof NativePaneRecoveryError))
|
|
1605
|
-
return false;
|
|
1606
|
-
this.instanceProcessStatus.set(name, "stopped");
|
|
1607
|
-
this.logger.warn({ name, recovery: error.recovery }, error.message);
|
|
1608
|
-
this.eventLog?.insert(name, "native_pane_recovery_needed", { ...error.recovery });
|
|
1609
|
-
this.needsYou?.poke();
|
|
1610
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1611
|
-
return true;
|
|
1612
|
-
}
|
|
1613
|
-
async startFreshNativePane(name, launchId, sessionId, authorized) {
|
|
1614
|
-
const config = this.fleetConfig?.instances[name];
|
|
1615
|
-
if (!isNativePaneInstance(config))
|
|
1616
|
-
throw Error("Persistent Windows pane is not configured");
|
|
1617
|
-
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1618
|
-
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1619
|
-
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1620
|
-
throw Error(t("native_pane.authority_expired")); };
|
|
1621
|
-
current();
|
|
1622
|
-
const held = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id).paneRecovery;
|
|
1623
|
-
if (held?.kind !== "session-unconfirmed" || held.launchId !== launchId || (held.sessionId ?? null) !== sessionId)
|
|
1624
|
-
throw Error(t("native_pane.changed"));
|
|
1625
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1626
|
-
throw Error(t("native_pane.still_running"));
|
|
1627
|
-
this.cancelStartupRetry(name);
|
|
1628
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1629
|
-
await this.lifecycle.stop(name, transition);
|
|
1630
|
-
current();
|
|
1631
|
-
try {
|
|
1632
|
-
await (await this.nativePanes()).startFresh(name, config, launchId, sessionId, current);
|
|
1633
|
-
current();
|
|
1634
|
-
}
|
|
1635
|
-
catch (error) {
|
|
1636
|
-
current();
|
|
1637
|
-
this.needsYou?.poke();
|
|
1638
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1639
|
-
throw error;
|
|
1640
|
-
}
|
|
1641
|
-
this.eventLog?.insert(name, "native_pane_new_conversation", { launchId, sessionId });
|
|
1642
|
-
await this.startInstance(name, config, true, "fleet-topic", true, transition);
|
|
1643
|
-
current();
|
|
1644
|
-
this.needsYou?.poke();
|
|
1645
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
nativePanePinnedSession(name) {
|
|
1649
|
-
const config = this.fleetConfig?.instances[name];
|
|
1650
|
-
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
1651
|
-
return null;
|
|
1652
|
-
const agent = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
1653
|
-
return agent.fleet_instance === name && agent.machineId === config.machine_id ? agent.threadId ?? null : null;
|
|
1654
|
-
}
|
|
1655
|
-
onNativePaneRecoverySettled(name) {
|
|
1656
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1657
|
-
this.instanceProcessStatus.delete(name);
|
|
1658
|
-
this.onAttentionChanged(name);
|
|
1659
|
-
this.scheduleDeliveryOutboxPump();
|
|
1660
|
-
}
|
|
1661
|
-
setNativePanePaused(name, paused) {
|
|
1662
|
-
const id = this.fleetConfig?.instances[name]?.native_agent_id;
|
|
1663
|
-
if (!id)
|
|
1664
|
-
throw Error("Native profile is missing");
|
|
1665
|
-
windowsAgentsFor(this, this.runtimeWindowsExecutor()).setPaused(id, paused);
|
|
1666
|
-
}
|
|
1667
|
-
async closeRetainedNativePane(name) {
|
|
1668
|
-
const config = this.fleetConfig?.instances[name];
|
|
1669
|
-
if (!config)
|
|
1670
|
-
throw Error("Native instance configuration is missing");
|
|
1671
|
-
await (await this.nativePanes()).closeRetained(name, config, () => { if (this.shuttingDown)
|
|
1672
|
-
throw Error("Fleet is stopping"); });
|
|
1673
|
-
}
|
|
1674
|
-
async reconcileNativePaneBeforeStart(name, daemon) {
|
|
1675
|
-
const outbox = this.deliveryOutbox;
|
|
1676
|
-
if (!outbox)
|
|
1677
|
-
return true;
|
|
1678
|
-
const epoch = this.lifecycle.epochOf(name);
|
|
1679
|
-
return daemon.reconcileNativeDeliveries(outbox, () => !this.shuttingDown && this.lifecycle.epochOf(name) === epoch);
|
|
1680
|
-
}
|
|
1681
|
-
/** M-2255: the Windows runtimes' switches the fleet's own state reflects (null until the start-up has applied them). */
|
|
1682
|
-
runtimeEffects = null;
|
|
1683
|
-
runtimeEffectsScheduled = false;
|
|
1684
|
-
localPaneDrain = null;
|
|
1685
|
-
remotePaneDrain = null;
|
|
1686
|
-
/** Revoke admissions synchronously; keep the captured transport only long enough for exact owned closes. */
|
|
1687
|
-
drainNativeRuntime(remote) {
|
|
1688
|
-
const existing = remote ? this.remotePaneDrain : this.localPaneDrain;
|
|
1689
|
-
if (existing)
|
|
1690
|
-
return existing;
|
|
1691
|
-
const hub = this.runtimeHub, relay = this.localPaneRelay;
|
|
1692
|
-
if (!remote)
|
|
1693
|
-
relay?.beginCleanup();
|
|
1694
|
-
const targets = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, config]) => isWindowsInstance(config) && !!config.machine_id === remote);
|
|
1695
|
-
for (const [name, config] of targets) {
|
|
1696
|
-
this.nativeMcpStore?.closeInstance(name);
|
|
1697
|
-
if (isNativePaneInstance(config)) {
|
|
1698
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1699
|
-
this.lifecycle.invalidate(name);
|
|
1700
|
-
this.daemons.get(name)?.fenceDeliveryWritesForStop();
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
const run = Promise.resolve().then(async () => {
|
|
1704
|
-
const results = await Promise.allSettled(targets.filter(([, config]) => isNativePaneInstance(config)).map(([name]) => this.stopInstance(name)));
|
|
1705
|
-
results.forEach((result, index) => { if (result.status === "rejected")
|
|
1706
|
-
this.logger.warn({ err: result.reason, remote, index }, "Windows pane cleanup unconfirmed; its reservation is retained"); });
|
|
1707
|
-
try {
|
|
1708
|
-
if (remote)
|
|
1709
|
-
await hub?.shutdownPaneHosts();
|
|
1710
|
-
else
|
|
1711
|
-
await relay?.shutdownHost();
|
|
1712
|
-
}
|
|
1713
|
-
catch (error) {
|
|
1714
|
-
this.logger.warn({ err: error, remote }, "Windows pane host shutdown unconfirmed; owned state is retained");
|
|
1715
|
-
}
|
|
1716
|
-
}).finally(() => {
|
|
1717
|
-
if (remote) {
|
|
1718
|
-
hub?.close();
|
|
1719
|
-
if (this.runtimeHub === hub)
|
|
1720
|
-
this.runtimeHub = null;
|
|
1721
|
-
}
|
|
1722
|
-
else {
|
|
1723
|
-
relay?.stop();
|
|
1724
|
-
if (this.localPaneRelay === relay)
|
|
1725
|
-
this.localPaneRelay = null;
|
|
1726
|
-
closeWindowsBridge(this);
|
|
1727
|
-
}
|
|
1728
|
-
});
|
|
1729
|
-
if (remote)
|
|
1730
|
-
this.remotePaneDrain = run;
|
|
1731
|
-
else
|
|
1732
|
-
this.localPaneDrain = run;
|
|
1733
|
-
void run.finally(() => {
|
|
1734
|
-
if (remote) {
|
|
1735
|
-
if (this.remotePaneDrain === run)
|
|
1736
|
-
this.remotePaneDrain = null;
|
|
1737
|
-
}
|
|
1738
|
-
else if (this.localPaneDrain === run)
|
|
1739
|
-
this.localPaneDrain = null;
|
|
1740
|
-
if (!this.shuttingDown && (remote ? remoteRuntimeEnabled() : localWindowsCliEnabled())) {
|
|
1741
|
-
this.initializeNativeInstances();
|
|
1742
|
-
this.initializeNativeInstances(true);
|
|
1743
|
-
}
|
|
1744
|
-
try {
|
|
1745
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1746
|
-
}
|
|
1747
|
-
catch { }
|
|
1748
|
-
}).catch(error => this.logger.warn({ err: error }, "Windows runtime cleanup failed; retained ownership requires reconciliation"));
|
|
1749
|
-
return run;
|
|
1750
|
-
}
|
|
1751
|
-
/**
|
|
1752
|
-
* M-2255: Settings → Runtime's switches (fleet.yaml `runtimes`, or their environment override) in force — set each time
|
|
1753
|
-
* the configuration in force changes (a load, a save, a reload's rollback), next to the web authority's. Every gate reads
|
|
1754
|
-
* them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
|
|
1755
|
-
* sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
|
|
1756
|
-
*/
|
|
1757
|
-
/** S-51: the Hub switches follow the configuration in force (load, save, reload rollback), like the runtime ones. */
|
|
1758
|
-
observeHubSwitches() {
|
|
1759
|
-
this.observeHubAgentIncarnations();
|
|
1760
|
-
if (!this.hubEffectsReady || this.hubEffectsScheduled || this.shuttingDown)
|
|
1761
|
-
return;
|
|
1762
|
-
this.hubEffectsScheduled = true;
|
|
1763
|
-
queueMicrotask(() => { this.hubEffectsScheduled = false; void this.applyHubSwitches(); });
|
|
1764
|
-
}
|
|
1765
|
-
/** Reads the raw configuration at run time (the newest wins); off and never started: returns before any import. */
|
|
1766
|
-
async applyHubSwitches() {
|
|
1767
|
-
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1768
|
-
return;
|
|
1769
|
-
const raw = this.rawFleetConfig;
|
|
1770
|
-
const on = raw?.hub?.enabled === true || raw?.hub_member?.enabled === true;
|
|
1771
|
-
if (!on && !this.hubRuntime && !this.hubRuntimeInit)
|
|
1772
|
-
return;
|
|
1773
|
-
try {
|
|
1774
|
-
const runtime = this.hubRuntime ?? (await this.hubRuntimeCreated());
|
|
1775
|
-
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1776
|
-
return; // Stop owns it now (fenced in stopAll, closed in doStopAll)
|
|
1777
|
-
await this.observeHubMemberPairing(raw?.hub_member?.enabled === true);
|
|
1778
|
-
const current = this.rawFleetConfig;
|
|
1779
|
-
await runtime.apply({ hub: current?.hub, hub_member: current?.hub_member });
|
|
1780
|
-
}
|
|
1781
|
-
catch (err) {
|
|
1782
|
-
this.logger.warn({ err: err?.message ?? String(err) }, "Hub switches could not be applied");
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
/** Single flight: one import, one runtime, assigned once; every caller (and Stop) gets that same runtime. */
|
|
1786
|
-
hubRuntimeCreated() {
|
|
1787
|
-
this.hubRuntimeInit ??= (async () => {
|
|
1788
|
-
const { HubRuntime } = await import("./hub/runtime.js");
|
|
1789
|
-
const runtime = new HubRuntime({
|
|
1790
|
-
home: this.dataDir,
|
|
1791
|
-
log: (level, message, meta) => this.logger[level](meta ?? {}, message),
|
|
1792
|
-
onHoldsChanged: holds => { this.hubHolds = holds; this.needsYou?.poke(); },
|
|
1793
|
-
// S-51 PR 2b: the fleet's side of a Hub membership (pairing binding, local agents, attachment points).
|
|
1794
|
-
member: {
|
|
1795
|
-
// S3c's currentBinding: a fresh, fail-closed read of the pairing state (null unless consistent and connectable).
|
|
1796
|
-
boundHub: () => this.hubMemberBinding?.(this.dataDir)?.hubId ?? null,
|
|
1797
|
-
agents: () => this.hubMemberAgents(),
|
|
1798
|
-
// P1 representative mode (docs/design/hub-representative-agent.md): ordinary delivery + the report_result hook.
|
|
1799
|
-
representative: {
|
|
1800
|
-
quota: agent => this.hubAgentQuota(agent),
|
|
1801
|
-
deliver: input => this.deliverHubQuestion(input),
|
|
1802
|
-
agentToken: agent => this.hubAgentToken(agent),
|
|
1803
|
-
// attachments-v1: a question's files go into the agent's inbox; an answer's come from its own paths.
|
|
1804
|
-
files: { stateDir: this.dataDir,
|
|
1805
|
-
inboxDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "inbox") : null,
|
|
1806
|
-
scratchDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "hub-files") : null },
|
|
1807
|
-
attachReports: (handler, failed) => { this.hubReports = handler; this.hubDeliveryFailed = failed; },
|
|
1808
|
-
},
|
|
1809
|
-
attachExposures: api => this.attachHubMemberExposures(api),
|
|
1810
|
-
attachAttention: read => this.attachHubExecutionAttention(read),
|
|
1811
|
-
pokeAttention: () => this.needsYou?.poke(),
|
|
1812
|
-
linkState: state => { this.hubMemberLink = state; },
|
|
1813
|
-
},
|
|
1814
|
-
});
|
|
1815
|
-
// Stop began during the import: it holds this promise and closes the runtime; do not hand it out again.
|
|
1816
|
-
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1817
|
-
runtime.fence();
|
|
1818
|
-
else
|
|
1819
|
-
this.hubRuntime = runtime;
|
|
1820
|
-
return runtime;
|
|
1821
|
-
})();
|
|
1822
|
-
return this.hubRuntimeInit;
|
|
1823
|
-
}
|
|
1824
|
-
/**
|
|
1825
|
-
* S-51 PR 2b: while the member switch is on, the local owner's "Connect to a Hub" backend (S3c's pairing API) is
|
|
1826
|
-
* attached, paired or not; switched off, it is detached. A binding change re-composes the member side.
|
|
1827
|
-
*/
|
|
1828
|
-
async observeHubMemberPairing(on) {
|
|
1829
|
-
if (!on) {
|
|
1830
|
-
this.attachHubMemberPairing(null);
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
if (this.hubMemberPairingApi)
|
|
1834
|
-
return;
|
|
1835
|
-
const { createHubMemberPairingApi, currentBinding } = await import("./hub/member/pairing-api.js");
|
|
1836
|
-
if (this.shuttingDown || !this.hubEffectsReady || this.hubMemberPairingApi)
|
|
1837
|
-
return;
|
|
1838
|
-
this.hubMemberBinding = currentBinding;
|
|
1839
|
-
this.attachHubMemberPairing(createHubMemberPairingApi({
|
|
1840
|
-
home: this.dataDir,
|
|
1841
|
-
enabled: () => this.rawFleetConfig?.hub_member?.enabled === true,
|
|
1842
|
-
link: () => (this.hubMemberLink === "online" ? "online" : "offline"),
|
|
1843
|
-
hold: () => (this.hubMemberLink === "revoked" ? "revoked" : null),
|
|
1844
|
-
// Disconnect: stop the member client now (the runtime re-composes from the saved binding, fencing first).
|
|
1845
|
-
fence: () => { void this.hubRuntime?.refresh(); },
|
|
1846
|
-
// A claim saved, a disconnect tombstone or a cancel: re-compose the member side for the new binding.
|
|
1847
|
-
bindingChanged: () => { void this.hubRuntime?.refresh(); },
|
|
1848
|
-
// Disconnect withdraws every published exposure locally (each saved before this returns).
|
|
1849
|
-
withdrawAll: () => this.hubMemberWithdrawAll(),
|
|
1850
|
-
}));
|
|
1851
|
-
}
|
|
1852
|
-
/**
|
|
1853
|
-
* Disconnect: every published exposure is withdrawn and saved before the disconnect reports success. Through the
|
|
1854
|
-
* running member's API, or — held or not composed — directly in the member's own store; a failure here makes the
|
|
1855
|
-
* disconnect report "unsaved" (S3c), so consent never outlives the disconnect.
|
|
1856
|
-
*/
|
|
1857
|
-
async hubMemberWithdrawAll() {
|
|
1858
|
-
const api = this.hubMemberExposureApi;
|
|
1859
|
-
if (api) {
|
|
1860
|
-
for (const view of api.list()) {
|
|
1861
|
-
if (view.exposureId && view.state === "published")
|
|
1862
|
-
await api.withdraw(view.exposureId, view.revision);
|
|
1863
|
-
}
|
|
1864
|
-
return;
|
|
1865
|
-
}
|
|
1866
|
-
const hubId = this.hubMemberPairingApi?.boundHub()?.hubId ?? null;
|
|
1867
|
-
if (!hubId)
|
|
1868
|
-
return;
|
|
1869
|
-
const { withdrawMemberExposuresOffline } = await import("./hub/compose/member.js");
|
|
1870
|
-
await withdrawMemberExposuresOffline(this.dataDir, hubId);
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* The Hub exposure picker's agents (docs/design/hub-representative-agent.md): every configured instance normal
|
|
1874
|
-
* delivery addresses, General included, shown by its display name. The dedicated Hub daemon is not a fleet instance.
|
|
1875
|
-
*/
|
|
1876
|
-
hubMemberAgents() {
|
|
1877
|
-
return Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({ name, display: this.shownInstanceName(name) }));
|
|
1878
|
-
}
|
|
1879
|
-
/** Same cached usage source as the owner's rail, scoped to the representative's CLI and credential profile. */
|
|
1880
|
-
async hubAgentQuota(agent) {
|
|
1881
|
-
const binding = () => {
|
|
1882
|
-
const config = this.fleetConfig?.instances[agent];
|
|
1883
|
-
if (!config || isWindowsInstance(config))
|
|
1884
|
-
return null; // runner usage never belongs to a Linux login
|
|
1885
|
-
const backend = this.backendNameForInstance(agent), provider = usageProviderIdForBackend(backend);
|
|
1886
|
-
if (!provider)
|
|
1887
|
-
return null;
|
|
1888
|
-
const profile = this.credentialProfileOf(agent, backend);
|
|
1889
|
-
return profile ? `${provider}:${profile}` : provider;
|
|
1890
|
-
};
|
|
1891
|
-
const id = binding();
|
|
1892
|
-
const { quotaBand } = await import("./hub/member/quota.js");
|
|
1893
|
-
const paused = () => this.daemons.get(agent)?.quotaPaused?.() === true
|
|
1894
|
-
|| this.lifecycle.isPaused(agent) && readPauseReason(this.getInstanceDir(agent)) === "quota";
|
|
1895
|
-
if (!id)
|
|
1896
|
-
return { band: "unknown" };
|
|
1897
|
-
try {
|
|
1898
|
-
const snapshot = await getUsageSnapshot(false, new Set([id]));
|
|
1899
|
-
if (binding() !== id)
|
|
1900
|
-
return { band: "unknown" };
|
|
1901
|
-
return quotaBand(snapshot.providers.find(p => p.id === id), paused());
|
|
1902
|
-
}
|
|
1903
|
-
catch {
|
|
1904
|
-
return quotaBand(undefined, paused());
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* A published representative's lifecycle token (#3145, #3147 review): a DURABLE incarnation id per agent name, rotated
|
|
1909
|
-
* whenever its backend, folder or runtime changes and dropped when the instance is removed. A replacement (remove and
|
|
1910
|
-
* re-add, even with the same name and config, across restarts) or any retarget — including A→B→A — gets a new id, so
|
|
1911
|
-
* earlier consent never matches again without an explicit republish. Stopping or pausing changes nothing.
|
|
1912
|
-
*/
|
|
1913
|
-
hubAgentToken(agent) {
|
|
1914
|
-
const instances = this.fleetConfig?.instances ?? {};
|
|
1915
|
-
if (!Object.prototype.hasOwnProperty.call(instances, agent))
|
|
1916
|
-
return null;
|
|
1917
|
-
if (this.hubIncarnationsBroken)
|
|
1918
|
-
return null;
|
|
1919
|
-
let id;
|
|
1920
|
-
try {
|
|
1921
|
-
this.settleHubIncarnations();
|
|
1922
|
-
id = this.hubIncarnationsBroken ? null : this.hubAgentIncarnation(agent, instances[agent], true);
|
|
1923
|
-
}
|
|
1924
|
-
catch (err) {
|
|
1925
|
-
this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be saved; the agent is not a deliverable representative");
|
|
1926
|
-
return null;
|
|
1927
|
-
}
|
|
1928
|
-
return id ? createHash("sha256").update(JSON.stringify(["agend-hub-agent-v2", agent, id])).digest("hex") : null;
|
|
1929
|
-
}
|
|
1930
|
-
hubIncarnationsPath() { return join(this.dataDir, "hub-member", "agent-incarnations.json"); }
|
|
1931
|
-
/**
|
|
1932
|
-
* M-2451 (#3147 residual): written beside hub-member/, in the data directory itself, when an invalidation could not be
|
|
1933
|
-
* made durable in hub-member/. A later process that finds it invalidates every incarnation before handing out any
|
|
1934
|
-
* token, so a restart never revives consent the earlier process had to withdraw (A→B→A, or a removal, while hub-member/
|
|
1935
|
-
* was unwritable).
|
|
1936
|
-
*/
|
|
1937
|
-
hubIncarnationsMarkerPath() { return join(this.dataDir, "hub-member-invalidate.pending"); }
|
|
1938
|
-
/** Whether this process has settled the incarnations on disk (the marker handled, the file proven writable). */
|
|
1939
|
-
hubIncarnationsSettled = false;
|
|
1940
|
-
/**
|
|
1941
|
-
* Once per process, before the first token: an invalidation an earlier process could not persist (the marker) clears
|
|
1942
|
-
* every incarnation; and an existing file must be rewritable here, else this process is broken (no token at all) —
|
|
1943
|
-
* an incarnation this process could not rotate must never be handed out. Fails closed: any doubt marks it broken.
|
|
1944
|
-
*/
|
|
1945
|
-
settleHubIncarnations() {
|
|
1946
|
-
if (this.hubIncarnationsSettled || this.hubIncarnationsBroken || !this.dataDir)
|
|
1947
|
-
return;
|
|
1948
|
-
const path = this.hubIncarnationsPath(), marker = this.hubIncarnationsMarkerPath();
|
|
1949
|
-
if (existsSync(marker)) {
|
|
1950
|
-
try {
|
|
1951
|
-
unlinkSync(path);
|
|
1952
|
-
}
|
|
1953
|
-
catch (err) {
|
|
1954
|
-
if (err?.code !== "ENOENT") {
|
|
1955
|
-
this.hubIncarnationsBroken = true;
|
|
1956
|
-
this.logger.warn({ err: err?.message }, "An earlier Hub consent invalidation is still pending and cannot be applied; no Hub representative token is handed out");
|
|
1957
|
-
return;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
// The clear is durable; a marker that cannot be removed only clears again next time (over-invalidation, never revival).
|
|
1961
|
-
try {
|
|
1962
|
-
unlinkSync(marker);
|
|
1963
|
-
}
|
|
1964
|
-
catch { /* left: harmless */ }
|
|
1965
|
-
this.logger.warn("An earlier Hub consent invalidation was applied: every Hub representative needs a republish");
|
|
1966
|
-
}
|
|
1967
|
-
if (existsSync(path))
|
|
1968
|
-
this.writeHubIncarnations(this.readHubIncarnations()); // throws (broken) when hub-member/ is not writable
|
|
1969
|
-
this.hubIncarnationsSettled = true;
|
|
1970
|
-
} /**
|
|
1971
|
-
* #3147 r2: set when an invalidation could not be made durable in any way (neither the atomic write nor removing the
|
|
1972
|
-
* file worked). From then on this process hands out no representative token at all, and the transitions the fleet
|
|
1973
|
-
* controls (a config save, a removal) are refused instead of applied.
|
|
1974
|
-
*/
|
|
1975
|
-
hubIncarnationsBroken = false;
|
|
1976
|
-
readHubIncarnations() {
|
|
1977
|
-
try {
|
|
1978
|
-
const value = JSON.parse(readFileSync(this.hubIncarnationsPath(), "utf8"));
|
|
1979
|
-
return value && typeof value === "object" && !Array.isArray(value) ? Object.assign(Object.create(null), value) : Object.create(null);
|
|
1980
|
-
}
|
|
1981
|
-
catch {
|
|
1982
|
-
return Object.create(null);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
/**
|
|
1986
|
-
* Persist the incarnation map: "written", or — when the atomic write fails — "cleared": the whole file is removed, which
|
|
1987
|
-
* durably invalidates EVERY earlier incarnation (all consent then needs a republish; never a revival). Throws (and
|
|
1988
|
-
* marks the process broken) only when neither is possible.
|
|
1989
|
-
*/
|
|
1990
|
-
writeHubIncarnations(map) {
|
|
1991
|
-
const path = this.hubIncarnationsPath(), tmp = `${path}.${process.pid}.tmp`;
|
|
1992
|
-
try {
|
|
1993
|
-
mkdirSync(join(this.dataDir, "hub-member"), { recursive: true, mode: 0o700 });
|
|
1994
|
-
writeFileSync(tmp, JSON.stringify(map), { mode: 0o600 });
|
|
1995
|
-
renameSync(tmp, path);
|
|
1996
|
-
return "written";
|
|
1997
|
-
}
|
|
1998
|
-
catch (err) {
|
|
1999
|
-
try {
|
|
2000
|
-
rmSync(tmp, { recursive: false, force: true });
|
|
2001
|
-
}
|
|
2002
|
-
catch { /* a directory planted there: left alone */ }
|
|
2003
|
-
try {
|
|
2004
|
-
unlinkSync(path);
|
|
2005
|
-
}
|
|
2006
|
-
catch (gone) {
|
|
2007
|
-
if (gone?.code !== "ENOENT") {
|
|
2008
|
-
this.hubIncarnationsBroken = true;
|
|
2009
|
-
// M-2451: record the withdrawal outside hub-member/, so a restart does not revive what this process withdrew.
|
|
2010
|
-
try {
|
|
2011
|
-
writeFileSync(this.hubIncarnationsMarkerPath(), "", { mode: 0o600 });
|
|
2012
|
-
}
|
|
2013
|
-
catch { /* the data directory too: nothing durable is left */ }
|
|
2014
|
-
throw err;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
this.logger.warn({ err: err?.message }, "Hub agent incarnations could not be saved; every earlier Hub consent is invalidated (republish needed)");
|
|
2018
|
-
return "cleared";
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
/** The agent's durable incarnation id (create: make one if none; else null). A changed fingerprint rotates it. */
|
|
2022
|
-
hubAgentIncarnation(agent, cfg, create) {
|
|
2023
|
-
const fp = createHash("sha256").update(JSON.stringify([cfg.backend ?? "claude-code", cfg.working_directory, cfg.runtime ?? "", cfg.machine_id ?? ""])).digest("hex");
|
|
2024
|
-
const map = this.readHubIncarnations(), entry = Object.prototype.hasOwnProperty.call(map, agent) ? map[agent] : undefined;
|
|
2025
|
-
if (entry && entry.fp === fp && typeof entry.id === "string")
|
|
2026
|
-
return entry.id;
|
|
2027
|
-
if (!entry && !create)
|
|
2028
|
-
return null;
|
|
2029
|
-
map[agent] = { id: randomUUID(), fp };
|
|
2030
|
-
// A rotation that could only clear the file still invalidated the old id durably; the new one is made next time.
|
|
2031
|
-
return this.writeHubIncarnations(map) === "written" ? map[agent].id : null;
|
|
2032
|
-
}
|
|
2033
|
-
/**
|
|
2034
|
-
* Every configuration in force: rotate a recorded agent's incarnation when its backend/folder/runtime changed; drop
|
|
2035
|
-
* removed ones. strict (a config save the fleet is about to write): a failure to make that durable throws, so the save
|
|
2036
|
-
* is refused instead of applied. Otherwise (a load or reload of a hand-edited file) a failure is logged — the
|
|
2037
|
-
* invalidation then is the cleared file, or, if nothing could be written, this process hands out no token at all.
|
|
2038
|
-
*/
|
|
2039
|
-
observeHubAgentIncarnations(strict = false) {
|
|
2040
|
-
// Only against a loaded configuration: never forget every agent because none is loaded yet.
|
|
2041
|
-
if (!this.dataDir || !this.fleetConfig?.instances)
|
|
2042
|
-
return;
|
|
2043
|
-
try {
|
|
2044
|
-
this.settleHubIncarnations();
|
|
2045
|
-
if (this.hubIncarnationsBroken)
|
|
2046
|
-
throw new Error("Hub agent incarnations cannot be written here");
|
|
2047
|
-
if (!existsSync(this.hubIncarnationsPath()))
|
|
2048
|
-
return;
|
|
2049
|
-
const instances = this.fleetConfig.instances;
|
|
2050
|
-
for (const agent of Object.keys(this.readHubIncarnations())) {
|
|
2051
|
-
// Gone from the configuration in force (removed by hand in fleet.yaml, too): forgotten, never revived by a re-add.
|
|
2052
|
-
if (Object.prototype.hasOwnProperty.call(instances, agent))
|
|
2053
|
-
this.hubAgentIncarnation(agent, instances[agent], false);
|
|
2054
|
-
else
|
|
2055
|
-
this.forgetHubAgentIncarnation(agent, true);
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
catch (err) {
|
|
2059
|
-
this.logger?.warn({ err: err?.message }, "Hub agent incarnations could not be checked");
|
|
2060
|
-
if (strict)
|
|
2061
|
-
throw new Error(`Hub consent for a changed agent could not be invalidated durably; the change is not saved (${err?.message})`);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
/** A removed instance's incarnation goes with it (a later instance of the same name is a new one). strict: throw if not durable. */
|
|
2065
|
-
forgetHubAgentIncarnation(agent, strict = false) {
|
|
2066
|
-
try {
|
|
2067
|
-
if (!this.dataDir || !existsSync(this.hubIncarnationsPath()))
|
|
2068
|
-
return;
|
|
2069
|
-
const map = this.readHubIncarnations();
|
|
2070
|
-
if (!Object.prototype.hasOwnProperty.call(map, agent))
|
|
2071
|
-
return;
|
|
2072
|
-
delete map[agent];
|
|
2073
|
-
this.writeHubIncarnations(map);
|
|
2074
|
-
}
|
|
2075
|
-
catch (err) {
|
|
2076
|
-
this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be dropped");
|
|
2077
|
-
if (strict)
|
|
2078
|
-
throw new Error(`Hub consent for ${agent} could not be invalidated durably; refusing the change (${err?.message})`);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
/**
|
|
2082
|
-
* A Hub question to its representative agent: ordinary cross-instance delivery (idle gate, wake, retries), from the
|
|
2083
|
-
* reserved Hub sender with the Hub-issued correlation id. beforeInput runs immediately before the IPC hand-off.
|
|
2084
|
-
*/
|
|
2085
|
-
async deliverHubQuestion(input) {
|
|
2086
|
-
const { HUB_REPORT_TARGET } = await import("./hub/representative.js");
|
|
2087
|
-
if (this.shuttingDown || !this.fleetConfig?.instances[input.agent])
|
|
2088
|
-
return false;
|
|
2089
|
-
// Windows agents run through their own runtime, which has no pre-input hook: not a P1 representative.
|
|
2090
|
-
if (isNativeExecInstance(this.fleetConfig.instances[input.agent]))
|
|
2091
|
-
return false;
|
|
2092
|
-
const ipcMeta = {
|
|
2093
|
-
chat_id: "",
|
|
2094
|
-
message_id: `hubmsg-${input.correlationId.slice(4)}`,
|
|
2095
|
-
user: `instance:${HUB_REPORT_TARGET}`,
|
|
2096
|
-
user_id: `instance:${HUB_REPORT_TARGET}`,
|
|
2097
|
-
ts: new Date().toISOString(),
|
|
2098
|
-
thread_id: "",
|
|
2099
|
-
from_instance: HUB_REPORT_TARGET,
|
|
2100
|
-
from_display: "Hub",
|
|
2101
|
-
correlation_id: input.correlationId,
|
|
2102
|
-
request_kind: "query",
|
|
2103
|
-
requires_reply: "true",
|
|
2104
|
-
task_summary: "A question from the Hub",
|
|
2105
|
-
};
|
|
2106
|
-
const result = await this.deliverToInstance(input.agent, { type: "fleet_inbound", targetSession: input.agent, content: input.text, meta: ipcMeta }, { isCrossInstance: true, stillCurrent: input.stillCurrent, beforeInput: input.beforeInput });
|
|
2107
|
-
return result !== false;
|
|
2108
|
-
}
|
|
2109
|
-
/** S-51: why the Hub (or this fleet's Hub membership) is not running, if it is held. Empty while off or running. */
|
|
2110
|
-
getHubHolds() {
|
|
2111
|
-
return this.hubHolds;
|
|
2112
|
-
}
|
|
2113
|
-
hubBrowserCertificate() { return this.hubRuntime?.browserCertificateSettings() ?? null; }
|
|
2114
|
-
hubRetention() { return this.hubRuntime?.retentionSettings() ?? null; }
|
|
2115
|
-
observeRuntimeSwitches() {
|
|
2116
|
-
setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
|
|
2117
|
-
if (!this.runtimeEffects || this.runtimeEffectsScheduled)
|
|
2118
|
-
return;
|
|
2119
|
-
this.runtimeEffectsScheduled = true;
|
|
2120
|
-
queueMicrotask(() => { this.runtimeEffectsScheduled = false; this.applyRuntimeSwitches(); });
|
|
2121
|
-
}
|
|
2122
|
-
/**
|
|
2123
|
-
* Turned off: what it allowed ends now — the paired computers' connections (their agents show as offline) or this PC's
|
|
2124
|
-
* Windows process (a turn in progress is cancelled, as when the fleet stops), and those agents' tool sessions.
|
|
2125
|
-
* Turned on: the Windows agents are registered and their chat history read, as at start-up.
|
|
2126
|
-
*/
|
|
2127
|
-
applyRuntimeSwitches() {
|
|
2128
|
-
const was = this.runtimeEffects;
|
|
2129
|
-
if (!was || this.shuttingDown)
|
|
2130
|
-
return;
|
|
2131
|
-
const now = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
2132
|
-
if (was.windows_cli === now.windows_cli && was.remote_runtime === now.remote_runtime)
|
|
2133
|
-
return;
|
|
2134
|
-
this.runtimeEffects = now;
|
|
2135
|
-
try {
|
|
2136
|
-
if (was.remote_runtime && !now.remote_runtime) {
|
|
2137
|
-
void this.drainNativeRuntime(true);
|
|
2138
|
-
}
|
|
2139
|
-
if (was.windows_cli && !now.windows_cli) {
|
|
2140
|
-
void this.drainNativeRuntime(false);
|
|
2141
|
-
}
|
|
2142
|
-
if (!was.windows_cli && !was.remote_runtime && !this.localPaneDrain && !this.remotePaneDrain) {
|
|
2143
|
-
this.initializeNativeInstances();
|
|
2144
|
-
this.initializeNativeInstances(true);
|
|
2145
|
-
}
|
|
2146
|
-
this.logger.info({ windows_cli: now.windows_cli, remote_runtime: now.remote_runtime }, "Windows runtime switches applied");
|
|
2147
|
-
}
|
|
2148
|
-
catch (error) {
|
|
2149
|
-
this.logger.warn({ err: error }, "Windows runtime switches could not be applied in full");
|
|
2150
|
-
}
|
|
2151
|
-
try {
|
|
2152
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
2153
|
-
}
|
|
2154
|
-
catch { /* the next status frame carries it */ }
|
|
2155
|
-
}
|
|
2156
|
-
/** An unavailable opt-in runtime must not prevent the Linux fleet from starting or restoring its own history. */
|
|
2157
|
-
initializeNativeInstances(history = false) {
|
|
2158
|
-
if (!windowsCliEnabled())
|
|
2159
|
-
return;
|
|
2160
|
-
try {
|
|
2161
|
-
const runtime = this.nativeInstanceRuntime();
|
|
2162
|
-
if (history)
|
|
2163
|
-
runtime.importHistory();
|
|
2164
|
-
else
|
|
2165
|
-
runtime.register();
|
|
2166
|
-
}
|
|
2167
|
-
catch (error) {
|
|
2168
|
-
this.logger.warn({ err: error }, "Windows runtime state could not be loaded; affected native instances remain unavailable");
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
/** Native CLIs are headless: the manager owns the durable delivery generation instead of a tmux daemon. */
|
|
2172
|
-
deliveryBootId(name) {
|
|
2173
|
-
const config = this.fleetConfig?.instances[name];
|
|
2174
|
-
if (isNativeExecInstance(config)) {
|
|
2175
|
-
if (!this.nativeInstanceRuntime().available(name))
|
|
2176
|
-
return null;
|
|
2177
|
-
const machine = config.machine_id ? this.runtimeCoordinator().machine(config.machine_id) : null;
|
|
2178
|
-
return machine ? `runtime:${machine.id}:${machine.bootId ?? machine.generation}` : this.managerBootId;
|
|
2179
|
-
}
|
|
2180
|
-
return this.daemons.get(name)?.bootId ?? null;
|
|
2181
|
-
}
|
|
2182
771
|
rawFleetConfig = {};
|
|
2183
|
-
/**
|
|
2184
|
-
* S-51 (#3095, experimental, off by default): the Hub runtime. src/hub/runtime.js is imported only once a Hub switch
|
|
2185
|
-
* (hub.enabled / hub_member.enabled) is on; while both are off nothing Hub-related loads, opens or listens.
|
|
2186
|
-
*/
|
|
2187
|
-
hubRuntime = null;
|
|
2188
|
-
/** S3c's currentBinding reader, loaded with the pairing API (only while the member switch is on). */
|
|
2189
|
-
hubMemberBinding = null;
|
|
2190
|
-
/** The member link as the "Connect to a Hub" view shows it (S5's client lifecycle). */
|
|
2191
|
-
hubMemberLink = "offline";
|
|
2192
|
-
/** OutboundContext: the Hub representative's answer hook (null: no member service running, or not a Hub answer). */
|
|
2193
|
-
hubReport(input) {
|
|
2194
|
-
return this.hubReports?.(input) ?? null;
|
|
2195
|
-
}
|
|
2196
|
-
/** The member side's pane-failure hook for a Hub-issued correlation id (else null). */
|
|
2197
|
-
hubDeliveryFailed = null;
|
|
2198
|
-
/** The member side's report_result hook for HUB_REPORT_TARGET while a representative service runs (else null). */
|
|
2199
|
-
hubReports = null;
|
|
2200
|
-
/** The one in-flight creation (single flight): overlapping first observations share it, Stop waits for it. */
|
|
2201
|
-
hubRuntimeInit = null;
|
|
2202
|
-
hubEffectsReady = false;
|
|
2203
|
-
hubEffectsScheduled = false;
|
|
2204
|
-
hubHolds = [];
|
|
2205
772
|
rawFleetDocument = null;
|
|
2206
773
|
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
2207
774
|
fleetDocumentWrites = null;
|
|
@@ -2380,7 +947,6 @@ export class FleetManager {
|
|
|
2380
947
|
kiroMigrationRuns = new Map();
|
|
2381
948
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
2382
949
|
needsYou = null;
|
|
2383
|
-
hubExecutionAttention = null;
|
|
2384
950
|
missedOfflineStore = null;
|
|
2385
951
|
configHistory = null;
|
|
2386
952
|
configHistoryTimer = null;
|
|
@@ -2695,8 +1261,6 @@ export class FleetManager {
|
|
|
2695
1261
|
/** #2855: a trusted guest's searches, per account. */
|
|
2696
1262
|
guestSearches = new OfficeWriteLimiter(undefined, { perWindow: GUEST_SEARCHES.perWindow, windowMs: GUEST_SEARCHES.windowMs });
|
|
2697
1263
|
officeSharesInstance;
|
|
2698
|
-
/** #2861: when each instance was last talked to and answered (no words), for share links' dialogue box. */
|
|
2699
|
-
officeTalk = new OfficeTalk();
|
|
2700
1264
|
/**
|
|
2701
1265
|
* #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
|
|
2702
1266
|
* their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
|
|
@@ -2713,7 +1277,6 @@ export class FleetManager {
|
|
|
2713
1277
|
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
2714
1278
|
skins: this.officeSkins,
|
|
2715
1279
|
levels: () => this.officeLevelsSnapshot(),
|
|
2716
|
-
talk: scope => this.officeTalk.recent(scope),
|
|
2717
1280
|
},
|
|
2718
1281
|
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
2719
1282
|
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
@@ -3009,8 +1572,6 @@ export class FleetManager {
|
|
|
3009
1572
|
* authorizing revoked links and cookies until the fleet restarted.
|
|
3010
1573
|
*/
|
|
3011
1574
|
settingsGeneration = 0;
|
|
3012
|
-
/** #2942: the settings baseline the agent-requested Windows changes are checked against (set with the gate). */
|
|
3013
|
-
settingsBaselineReader = null;
|
|
3014
1575
|
settingsRuntimeRevision = 0;
|
|
3015
1576
|
settingsConfirmation = null;
|
|
3016
1577
|
settingsControl = null;
|
|
@@ -3067,29 +1628,8 @@ export class FleetManager {
|
|
|
3067
1628
|
return this.settingsConfirmation;
|
|
3068
1629
|
const generation = this.settingsGeneration;
|
|
3069
1630
|
const current = () => !this.shuttingDown && this.settingsGeneration === generation;
|
|
3070
|
-
const baselines =
|
|
1631
|
+
const baselines = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
|
|
3071
1632
|
config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
|
|
3072
|
-
generalLocation: input => this.previewGeneralLocation(input),
|
|
3073
|
-
hubMembership: raw => hubMembershipSettings(raw),
|
|
3074
|
-
// S3c (S-55): a Hub exposure publish's confirmation summary, while a member composition is attached.
|
|
3075
|
-
hubBrowserCertificate: (input, binding) => {
|
|
3076
|
-
const control = this.hubBrowserCertificate();
|
|
3077
|
-
if (!control)
|
|
3078
|
-
throw Error("unavailable");
|
|
3079
|
-
return control.preview(input, binding);
|
|
3080
|
-
},
|
|
3081
|
-
hubExposure: (input) => {
|
|
3082
|
-
const api = this.hubMemberExposureApi;
|
|
3083
|
-
if (!api)
|
|
3084
|
-
throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
|
|
3085
|
-
return api.preview(input);
|
|
3086
|
-
},
|
|
3087
|
-
hubPairing: (action, input) => {
|
|
3088
|
-
const api = this.hubMemberPairingApi;
|
|
3089
|
-
if (!api)
|
|
3090
|
-
throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
|
|
3091
|
-
return pairingSummary(action, input, api.view(), api.boundHub());
|
|
3092
|
-
},
|
|
3093
1633
|
proof: (kind, target, id, binding, key) => {
|
|
3094
1634
|
const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
|
|
3095
1635
|
const proof = map.get(id);
|
|
@@ -3110,17 +1650,6 @@ export class FleetManager {
|
|
|
3110
1650
|
const store = new SettingsConfirmationStore({
|
|
3111
1651
|
audit: (event, fields) => this.logger.info({ event: `settings_confirmation.${event}`, ...fields }, "Settings confirmation audit"),
|
|
3112
1652
|
notify: view => view.state === "pending" ? this.promptSettingsChange(store, view) : Promise.resolve(),
|
|
3113
|
-
// alpha.2 (M-2300): an agent's request came, was decided or ran out — the open pages read the list again (a ping
|
|
3114
|
-
// with nothing in it: only a web-only fleet's verified owner may read it, GET /ui/agent-proposals).
|
|
3115
|
-
changed: view => {
|
|
3116
|
-
if (view.source !== "agent_request")
|
|
3117
|
-
return;
|
|
3118
|
-
this.agentProposalsRev++;
|
|
3119
|
-
broadcastSseEvent(this.sseClients, "agent_proposals", {}, err => this.logger.debug({ err }, "agent_proposals event failed"));
|
|
3120
|
-
// …and a status frame with the new revision: a tab from before the upgrade (its stream knows no `agent_proposals`)
|
|
3121
|
-
// applies status frames, and a polling page reads the same frame at its next /ui/poll.
|
|
3122
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
3123
|
-
},
|
|
3124
1653
|
});
|
|
3125
1654
|
const gate = new SettingsHttpConfirmation(store, {
|
|
3126
1655
|
principal: req => {
|
|
@@ -3134,7 +1663,6 @@ export class FleetManager {
|
|
|
3134
1663
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
3135
1664
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
3136
1665
|
selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
|
|
3137
|
-
directAgentCreation: true,
|
|
3138
1666
|
job: async (id) => {
|
|
3139
1667
|
const settlement = this.settingsJobSettlements.get(id);
|
|
3140
1668
|
if (!settlement)
|
|
@@ -3169,10 +1697,8 @@ export class FleetManager {
|
|
|
3169
1697
|
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
3170
1698
|
if (auth.kind !== "ok")
|
|
3171
1699
|
return null;
|
|
3172
|
-
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
3173
|
-
|
|
3174
|
-
// opened): the web-only fleet's owner, judged below by the same authority as here (hostOperator).
|
|
3175
|
-
if (gatewayRequestContext(req) && auth.session.remote !== "totp")
|
|
1700
|
+
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
1701
|
+
if (gatewayRequestContext(req))
|
|
3176
1702
|
return null;
|
|
3177
1703
|
// …and never a change that turns the named-link option on: that one needs a General's card or the host.
|
|
3178
1704
|
if (!store.selfConfirmable(id))
|
|
@@ -3205,223 +1731,30 @@ export class FleetManager {
|
|
|
3205
1731
|
if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
|
|
3206
1732
|
return null;
|
|
3207
1733
|
const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
|
|
3208
|
-
|
|
3209
|
-
if (!destinations.length)
|
|
1734
|
+
if (!this.settingsNoticeGenerals(affected, chat.platform).length)
|
|
3210
1735
|
return null;
|
|
3211
1736
|
const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
|
|
3212
1737
|
return {
|
|
3213
1738
|
label: t("settings_confirm.web_verified_label", name),
|
|
3214
1739
|
// admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
|
|
3215
|
-
current: () => !!verified() && chat.admin(actor.user_id) &&
|
|
3216
|
-
followStop: async (name, begin) => { const route = destinations.find(item => item.current()); if (!route)
|
|
3217
|
-
throw Error("Settings route expired"); await route.followStop(name, begin); },
|
|
1740
|
+
current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
|
|
3218
1741
|
done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
|
|
3219
1742
|
};
|
|
3220
1743
|
}
|
|
3221
|
-
/**
|
|
3222
|
-
* alpha.2 (M-2300; #2973's agent requests, junsion's §6): on a fleet with no chat connection there is no General to
|
|
3223
|
-
* post an agent's Windows request in — it waited for `agend settings confirm` on the host. Its owner (decision
|
|
3224
|
-
* 28c2d3a3: the `agend web --code` session on this computer, web-verified) now sees and decides it from the web's
|
|
3225
|
-
* Needs you. The authority is asked from the request each time, and again at execution by the store (`current`):
|
|
3226
|
-
* still that owner now (this computer, not revoked, signed out or run out), still a fleet with no chat connection,
|
|
3227
|
-
* never over the public link. Anyone else — a guest, a LAN or public-link session, a chat-connected fleet's admin —
|
|
3228
|
-
* gets nothing (a chat fleet keeps General's card as its way).
|
|
3229
|
-
*/
|
|
3230
|
-
/**
|
|
3231
|
-
* #2997 review (Prism P2): how many times an agent's request came or was decided — in a web-only fleet's status frame,
|
|
3232
|
-
* so a page that never hears the stream's ping (one polling /ui/poll, one reconnecting, a tab from before the upgrade)
|
|
3233
|
-
* reads the list again when it moves. A count, nothing more.
|
|
3234
|
-
*/
|
|
3235
|
-
agentProposalsRev = 0;
|
|
3236
|
-
agentProposalOwner(req) {
|
|
3237
|
-
// #3008: this computer's admin here, or signed in from outside (over the link connections from outside opened) —
|
|
3238
|
-
// asked of the LIVE session each time (#2997 review): a sign-out, an expiry, a revocation or the switch going off
|
|
3239
|
-
// while the request is applied fences it.
|
|
3240
|
-
return this.liveOwner(req, { hostOnly: false });
|
|
3241
|
-
}
|
|
3242
|
-
/**
|
|
3243
|
-
* Refs #3061 (P2a): the new-device approval's one authority — the same one that decides an agent's request: this web-only
|
|
3244
|
-
* fleet's owner, asked of the LIVE session each time (a sign-out, an expiry, a revocation, the authority switch or
|
|
3245
|
-
* "Only this computer can authorize" turning a session into a non-owner is seen at once). A LAN session that signed in
|
|
3246
|
-
* with a one-time code, a session a device approval made, a guest, a chat fleet's admin and the CLI's header token are
|
|
3247
|
-
* none of them this: they are told so, and learn nothing about any request.
|
|
3248
|
-
*/
|
|
3249
|
-
deviceApprovalOwner(req) {
|
|
3250
|
-
return this.liveOwner(req, { hostOnly: false });
|
|
3251
|
-
}
|
|
3252
|
-
/**
|
|
3253
|
-
* Refs #3061 (auth-api): device-code sign-in is offered — a web-only fleet with web authorization on. With
|
|
3254
|
-
* `web.verified_authority.enabled: false` no one is an owner (liveOwner → null), so no one could approve: the three
|
|
3255
|
-
* `/auth/device-login*` routes answer 404 and the sign-in page shows no button. "Only this computer can authorize"
|
|
3256
|
-
* does not close it: this computer's own session can still approve.
|
|
3257
|
-
*/
|
|
3258
|
-
deviceLoginAvailable() {
|
|
3259
|
-
return this.isWebOnlyFleet() && this.webAuthorityState().enabled;
|
|
3260
|
-
}
|
|
3261
|
-
/** Refs #3061 (P2b): `web.login_qr` (default off), read per page served — the sign-in page then draws the approve link's QR code. */
|
|
3262
|
-
loginQrEnabled() {
|
|
3263
|
-
return this.fleetConfig?.web?.login_qr === true;
|
|
3264
|
-
}
|
|
3265
|
-
/**
|
|
3266
|
-
* GET /ui/devices: is this a web-only fleet, may THIS session approve a new device and end another (is it the verified
|
|
3267
|
-
* owner), and — to an admin session, never to a guest or the CLI's header token — the signed-in devices (Refs #3061, P2b).
|
|
3268
|
-
* `authority`: web authorization is on. With it off nobody is an owner (liveOwner → null), so nobody may add a device or
|
|
3269
|
-
* sign another out; the page says why and points to `agend web-token rotate` on this computer instead of "use the AgEnD
|
|
3270
|
-
* computer", which would not help.
|
|
3271
|
-
*/
|
|
3272
|
-
webDevicesStatus(req) {
|
|
3273
|
-
const webOnly = this.isWebOnlyFleet();
|
|
3274
|
-
const owner = webOnly && !!this.deviceApprovalOwner(req);
|
|
3275
|
-
return { webOnly, owner, authority: webOnly && this.webAuthorityState().enabled, devices: webOnly ? this.webDeviceRows(req) : [] };
|
|
3276
|
-
}
|
|
3277
|
-
/**
|
|
3278
|
-
* Refs #3061 (P2b): the device list — this fleet's admin sessions, newest activity first (a guest's sessions are the
|
|
3279
|
-
* Guests page's, and a read-only tier has none): how each was approved, whether it carries owner authority, and which
|
|
3280
|
-
* one is the reader's own. The same people may read this as may read `GET /auth/sessions` (any admin session).
|
|
3281
|
-
*
|
|
3282
|
-
* `ownerVerified` is the §3 verdict for the session as it would be used from where it signs in (a host session from
|
|
3283
|
-
* this computer, a session from outside over the link): one that signed in with a one-time code or a device approval is
|
|
3284
|
-
* not an owner until the authenticator vouches for it. Display only — the checks that act on it run again at each use.
|
|
3285
|
-
*/
|
|
3286
|
-
webDeviceRows(req) {
|
|
3287
|
-
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
3288
|
-
if (auth.kind !== "ok" || auth.session.tier !== "admin" || !this.webSessions)
|
|
3289
|
-
return [];
|
|
3290
|
-
const authorizations = this.sessionAuthorizationStore();
|
|
3291
|
-
return this.webSessions.list(auth.session.idHash, r => r.tier === "admin").map(summary => {
|
|
3292
|
-
const record = this.webSessions.findByHandle(summary.handle);
|
|
3293
|
-
const gateway = summary.surface === "gateway";
|
|
3294
|
-
const peer = gateway ? authorizations?.get(summary.handle)?.peer ?? "quick_link" : record?.host === true ? "loopback" : "lan";
|
|
3295
|
-
return { handle: summary.handle, label: summary.label, via: summary.via, current: summary.current, surface: summary.surface,
|
|
3296
|
-
created: summary.created, lastSeen: summary.lastSeen, expiresAt: Math.min(summary.absoluteExpiry, summary.idleExpiry),
|
|
3297
|
-
ownerVerified: !!record && !this.webAuthority({ session: record, gateway, peer }).refusal };
|
|
3298
|
-
});
|
|
3299
|
-
}
|
|
3300
|
-
/**
|
|
3301
|
-
* Refs #3061 (P2b): may this request's session end ANOTHER session — one signed in elsewhere, or all of them (auth-api
|
|
3302
|
-
* `DELETE /auth/sessions…`, which an admin session could always do). On a web-only fleet that is the verified owner's:
|
|
3303
|
-
* a session a code or a device approval made is signed in but not an owner, and could otherwise sign out the owner's
|
|
3304
|
-
* own. A fleet with a chat connection is as it was (its admins are the chat's). Ending one's OWN session is never this.
|
|
3305
|
-
*/
|
|
3306
|
-
mayEndOtherSessions(req) {
|
|
3307
|
-
return !this.isWebOnlyFleet() || !!this.deviceApprovalOwner(req);
|
|
3308
|
-
}
|
|
3309
|
-
/**
|
|
3310
|
-
* Refs #3061 (P2b): POST /ui/devices/signout {handle} — the owner ends another signed-in device (the settings list's
|
|
3311
|
-
* button). Only the verified owner, asked of the LIVE session each time (a non-owner learns nothing about any handle:
|
|
3312
|
-
* 403 before anything is looked up); only an admin session of this fleet (a guest's is the Guests page's; an unknown
|
|
3313
|
-
* handle and a guest's are the same 404); never the owner's own session here (Sign out is /auth/logout). The device's
|
|
3314
|
-
* authorization goes with it, so a copy of its cookie is nobody's.
|
|
3315
|
-
*/
|
|
3316
|
-
webDeviceSignOut(req, handle) {
|
|
3317
|
-
if (!this.isWebOnlyFleet() || !this.webSessions)
|
|
3318
|
-
return { status: 404, body: { error: "not_found" } };
|
|
3319
|
-
const owner = this.deviceApprovalOwner(req);
|
|
3320
|
-
if (!owner)
|
|
3321
|
-
return { status: 403, body: { error: "owner_required" } };
|
|
3322
|
-
if (handle === owner.handle)
|
|
3323
|
-
return { status: 400, body: { error: "this_device" } };
|
|
3324
|
-
const target = this.webSessions.findByHandle(handle);
|
|
3325
|
-
if (!target || target.tier !== "admin")
|
|
3326
|
-
return { status: 404, body: { error: "not_found" } };
|
|
3327
|
-
const { found, durable } = this.webSessions.revokeByHandle(handle, r => r.tier === "admin");
|
|
3328
|
-
if (!found)
|
|
3329
|
-
return { status: 404, body: { error: "not_found" } };
|
|
3330
|
-
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
3331
|
-
this.logger.info({ by: owner.handle.slice(0, 8), handle, label: target.label, via: sessionVia(target), durable }, "Device signed out by the owner");
|
|
3332
|
-
if (!durable)
|
|
3333
|
-
return { status: 500, body: { ok: false, durable: false, error: "not_durable" } };
|
|
3334
|
-
return { status: 200, body: { ok: true } };
|
|
3335
|
-
}
|
|
3336
|
-
/**
|
|
3337
|
-
* Refs #3061 (P2a): look a new device's code up (for the confirm screen), approve it, or deny it — the verified owner's,
|
|
3338
|
-
* on a web-only fleet. The answer is the request as it now stands. Approving only marks the request approved: the new
|
|
3339
|
-
* browser makes its own session when it next asks (auth-api POST /auth/device-login/poll), so no credential passes from
|
|
3340
|
-
* here to there — not in the answer, not through the fleet. A wrong code counts against one fleet-wide breaker; while
|
|
3341
|
-
* it is open every code (a right one too) waits.
|
|
3342
|
-
*/
|
|
3343
|
-
webDeviceLoginAct(req, op, code) {
|
|
3344
|
-
const owner = this.deviceApprovalOwner(req);
|
|
3345
|
-
if (!owner)
|
|
3346
|
-
return { status: 403, body: { error: "owner_required" } };
|
|
3347
|
-
const logins = this.webDeviceLogins, token = this.webToken;
|
|
3348
|
-
if (!logins || !token)
|
|
3349
|
-
return { status: 404, body: { error: "not_found" } };
|
|
3350
|
-
const epoch = tokenEpoch(token);
|
|
3351
|
-
const shown = (v) => ({ code: v.code, label: v.label, peer: v.peer, address: v.address, expiresAt: v.expiresAt, state: v.state });
|
|
3352
|
-
if (op === "lookup") {
|
|
3353
|
-
const r = logins.lookup(code, epoch);
|
|
3354
|
-
if (r.kind === "ok")
|
|
3355
|
-
return { status: 200, body: { request: shown(r.view) } };
|
|
3356
|
-
return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
|
|
3357
|
-
}
|
|
3358
|
-
// (The owner above was asked of the live session just now, after the body was read; nothing is awaited since.)
|
|
3359
|
-
const r = logins.decide(code, op === "approve" ? "approved" : "denied", owner.handle, epoch);
|
|
3360
|
-
if (r.kind === "ok") {
|
|
3361
|
-
this.logger.info({ approvedBy: owner.handle.slice(0, 8), label: r.view.label, peer: r.view.peer, decision: r.view.state }, op === "approve" ? "New device sign-in approved" : "New device sign-in denied");
|
|
3362
|
-
return { status: 200, body: { request: shown(r.view) } };
|
|
3363
|
-
}
|
|
3364
|
-
if (r.kind === "decided")
|
|
3365
|
-
return { status: 409, body: { error: "already_decided", state: r.state } };
|
|
3366
|
-
return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
|
|
3367
|
-
}
|
|
3368
|
-
/** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
|
|
3369
|
-
webAgentProposals(req) {
|
|
3370
|
-
if (!this.agentProposalOwner(req))
|
|
3371
|
-
return null;
|
|
3372
|
-
return this.settingsGate().store.listBySource("agent_request");
|
|
3373
|
-
}
|
|
3374
|
-
/**
|
|
3375
|
-
* alpha.2: the owner's Approve or Reject of an agent's request. The store claims it before its first await (a second
|
|
3376
|
-
* click, from here or the host, finds it no longer pending) and asks the owner, the request (its fleet generation and
|
|
3377
|
-
* Windows switch) and the target machine again before and while it runs the same creation code the web uses. The
|
|
3378
|
-
* answer is always the request as it now stands: applied (with the new agent's name), rejected, expired, stale or
|
|
3379
|
-
* failed (with its reason), or already decided.
|
|
3380
|
-
*/
|
|
3381
|
-
async webDecideAgentProposal(req, id, decision) {
|
|
3382
|
-
const owner = this.agentProposalOwner(req);
|
|
3383
|
-
if (!owner)
|
|
3384
|
-
return { status: 403, body: { error: "owner_required", message: t("settings_confirm.web_owner_required") } };
|
|
3385
|
-
const store = this.settingsGate().store, before = store.inspect(id);
|
|
3386
|
-
if (!before || before.source !== "agent_request")
|
|
3387
|
-
return { status: 404, body: { error: "not_found", message: t("settings_confirm.no_longer_pending") } };
|
|
3388
|
-
try {
|
|
3389
|
-
const view = await store.decide(id, decision, { ...owner, via: "web" });
|
|
3390
|
-
this.logger.info({ id, decision, state: view.state }, "Agent request decided from the web by this computer's admin");
|
|
3391
|
-
return { status: 200, body: { view, agent: agentNameOf(view) } };
|
|
3392
|
-
}
|
|
3393
|
-
catch (err) {
|
|
3394
|
-
const code = err instanceof SettingsConfirmationError ? err.code : "decide_failed";
|
|
3395
|
-
const status = err instanceof SettingsConfirmationError ? err.status : 500;
|
|
3396
|
-
// Already decided (a double click, the host's command), run out or withdrawn: say what it is now.
|
|
3397
|
-
return { status, body: { error: code, message: t(code === "admin_required" ? "settings_confirm.web_owner_required" : "settings_confirm.no_longer_pending"), view: store.inspect(id) } };
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
/** A confirmation belongs to the server route/lifetime, not the availability of General's model. */
|
|
3401
|
-
settingsGeneralRoute(name, platform, excluded) {
|
|
3402
|
-
const config = this.fleetConfig?.instances[name];
|
|
3403
|
-
if (!config?.general_topic || this.shuttingDown || this.ipcStoppingInstances.has(name))
|
|
3404
|
-
return null;
|
|
3405
|
-
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
3406
|
-
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform)
|
|
3407
|
-
return null;
|
|
3408
|
-
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
3409
|
-
if (!chatId || !topic)
|
|
3410
|
-
return null;
|
|
3411
|
-
const route = () => !this.shuttingDown && this.fleetConfig?.instances[name]?.general_topic === true
|
|
3412
|
-
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
3413
|
-
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
3414
|
-
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic;
|
|
3415
|
-
const lifetime = new SettingsRouteLifetime(name, route, () => this.lifecycle.epochOf(name), () => this.ipcStoppingInstances.has(name));
|
|
3416
|
-
return lifetime.current() ? { adapterId, adapter, chatId, topic, current: lifetime.current, followStop: lifetime.followStop.bind(lifetime) } : null;
|
|
3417
|
-
}
|
|
3418
|
-
/** Connected, same-platform, unaffected server routes can post cards even while the model is offline. */
|
|
1744
|
+
/** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
|
|
3419
1745
|
settingsNoticeGenerals(affected, platform) {
|
|
3420
|
-
const excluded = new Set(affected);
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
1746
|
+
const excluded = new Set(affected), found = [];
|
|
1747
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
1748
|
+
if (!config.general_topic || !this.daemons.get(name))
|
|
1749
|
+
continue;
|
|
1750
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1751
|
+
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform || this.adapterState.get(adapterId)?.status !== "connected")
|
|
1752
|
+
continue;
|
|
1753
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1754
|
+
if (chatId && topic)
|
|
1755
|
+
found.push({ adapterId, adapter, chatId, topic });
|
|
1756
|
+
}
|
|
1757
|
+
return found;
|
|
3425
1758
|
}
|
|
3426
1759
|
/**
|
|
3427
1760
|
* #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
|
|
@@ -3480,119 +1813,6 @@ export class FleetManager {
|
|
|
3480
1813
|
return { platform, current, admin: userId => current() && !!userId
|
|
3481
1814
|
&& identities.every(owner => this.isFleetAdmin(userId, owner.id)) };
|
|
3482
1815
|
}
|
|
3483
|
-
/**
|
|
3484
|
-
* #2942: a Windows create or settings change an AGENT asked for. Nothing is created or written here: it becomes a
|
|
3485
|
-
* pending settings change whose General card a fleet admin must confirm (promptSettingsChange — the same admin rule
|
|
3486
|
-
* as every card). Its apply runs the same creation/patch code the web uses, inside the store's SettingsExecution, with
|
|
3487
|
-
* the confirming admin, the Windows switch and the target machine asked again at that moment.
|
|
3488
|
-
*/
|
|
3489
|
-
async proposeAgentWindowsChange(input) {
|
|
3490
|
-
const gate = this.settingsGate(), baselines = this.settingsBaselineReader;
|
|
3491
|
-
if (!baselines || !this.fleetConfig)
|
|
3492
|
-
throw new Error("Fleet configuration is unavailable");
|
|
3493
|
-
const generation = this.settingsGeneration, machineId = input.machineId;
|
|
3494
|
-
const switchOn = () => machineId ? remoteRuntimeEnabled() : localWindowsCliEnabled();
|
|
3495
|
-
const online = () => !machineId || (remoteRuntimeEnabled() && this.runtimeCoordinator().machine(machineId)?.status === "online");
|
|
3496
|
-
if (!switchOn())
|
|
3497
|
-
throw new Error(machineId ? "Remote runtime is not enabled" : LOCAL_WINDOWS_CLI_OFF);
|
|
3498
|
-
const baseline = await baselines.read(), snapshot = settingsFingerprint(baselines.snapshot());
|
|
3499
|
-
const { view } = gate.store.propose({ session: `agent:${input.requester}`, key: `agent-windows:${input.key}`,
|
|
3500
|
-
requestFingerprint: input.fingerprint, fingerprint: input.fingerprint, section: "channel", requestedBy: input.requester,
|
|
3501
|
-
source: "agent_request", summary: input.summary, bytes: input.bytes, authority: input.authority, affectedConnections: [],
|
|
3502
|
-
current: () => !this.shuttingDown && this.settingsGeneration === generation && switchOn(),
|
|
3503
|
-
snapshot: () => baselines.snapshot(),
|
|
3504
|
-
unchanged: async () => (await baselines.read()).fingerprint === baseline.fingerprint && settingsFingerprint(baselines.snapshot()) === snapshot,
|
|
3505
|
-
// The store's own fence (this confirming admin, the session, the state) plus the switch and machine now.
|
|
3506
|
-
apply: async (actorCurrent, execution) => {
|
|
3507
|
-
execution.assert();
|
|
3508
|
-
// alpha.2 polish: a paired machine whose CLI inventory has not arrived yet (just paired) is asked for its own
|
|
3509
|
-
// report first (it runs on that PC), so the creation validates against what is installed there — never a refusal
|
|
3510
|
-
// that only a manual Refresh would undo. Its answer or failure changes nothing else; the fence is asked again.
|
|
3511
|
-
if (machineId && online() && !inventoryProbed(this.runtimeCoordinator().machine(machineId)?.inventory)) {
|
|
3512
|
-
// `force`: the runner's cached answer on a fresh profile is the placeholder itself (#3005 rig recheck).
|
|
3513
|
-
try {
|
|
3514
|
-
await this.runtimeCoordinator().call(machineId, "probe", { force: true });
|
|
3515
|
-
}
|
|
3516
|
-
catch { /* validation says what is missing */ }
|
|
3517
|
-
execution.assert();
|
|
3518
|
-
}
|
|
3519
|
-
return input.apply(() => actorCurrent() && online(), execution);
|
|
3520
|
-
} });
|
|
3521
|
-
return view;
|
|
3522
|
-
}
|
|
3523
|
-
/** #2942: create_instance with runtime "windows" from an agent: validated like the web, then held for General. */
|
|
3524
|
-
async requestWindowsCreate(requester, rawArgs) {
|
|
3525
|
-
const cfg = this.fleetConfig;
|
|
3526
|
-
if (!cfg)
|
|
3527
|
-
throw new Error("Fleet configuration is unavailable");
|
|
3528
|
-
let parsed;
|
|
3529
|
-
try {
|
|
3530
|
-
parsed = parseNativeInstanceCreate(rawArgs);
|
|
3531
|
-
}
|
|
3532
|
-
catch {
|
|
3533
|
-
throw new Error("Invalid Windows agent request: check the directory, sandbox, backend and model");
|
|
3534
|
-
}
|
|
3535
|
-
// The web's own projection: the same validation, topic target and diff the admin's dashboard confirmation shows.
|
|
3536
|
-
let diff;
|
|
3537
|
-
try {
|
|
3538
|
-
diff = prepareSettingsEffect("POST", "/ui/instances", rawArgs, { config: cfg, classic: {} }).diff;
|
|
3539
|
-
}
|
|
3540
|
-
catch {
|
|
3541
|
-
throw new Error("Invalid Windows agent request: its topic connection is not one this fleet can use");
|
|
3542
|
-
}
|
|
3543
|
-
if (!diff)
|
|
3544
|
-
throw new Error("Invalid Windows agent request");
|
|
3545
|
-
return this.proposeAgentWindowsChange({ requester, key: diff.fingerprint, fingerprint: diff.fingerprint, summary: diff.summary,
|
|
3546
|
-
machineId: parsed.input.machineId, bytes: JSON.stringify(rawArgs).length,
|
|
3547
|
-
// The General card is the confirmation for an agent's request, whatever the requested connection (a topic or none).
|
|
3548
|
-
authority: { connections: diff.authority?.connections ?? [], primaryGeneral: true, unknown: false },
|
|
3549
|
-
apply: (allowed, execution) => createNativeWindowsInstance(this, parsed, allowed, execution) });
|
|
3550
|
-
}
|
|
3551
|
-
/** #2942: update_instance_config on a Windows agent: only the fields the web editor allows, held for General. */
|
|
3552
|
-
async requestWindowsConfig(requester, name, patch) {
|
|
3553
|
-
const inst = this.fleetConfig?.instances[name];
|
|
3554
|
-
if (!isWindowsInstance(inst))
|
|
3555
|
-
throw new Error("Windows agent not found");
|
|
3556
|
-
let edit;
|
|
3557
|
-
try {
|
|
3558
|
-
edit = NativeInstanceConfigEdit.parse(patch);
|
|
3559
|
-
}
|
|
3560
|
-
catch {
|
|
3561
|
-
throw new Error("Invalid Windows agent settings: display_name, description and model only");
|
|
3562
|
-
}
|
|
3563
|
-
const before = { display_name: inst.display_name ?? "", description: inst.description ?? "", model: inst.model ?? "" };
|
|
3564
|
-
const changed = Object.keys(edit).filter(key => edit[key] !== before[key]);
|
|
3565
|
-
if (!changed.length)
|
|
3566
|
-
throw new Error("Nothing to change: these settings already have these values");
|
|
3567
|
-
const summary = changed.map(key => `${name} ${key}: ${settingsDisplay(before[key])} → ${settingsDisplay(edit[key])}`);
|
|
3568
|
-
const machineId = typeof inst.machine_id === "string" ? inst.machine_id : undefined;
|
|
3569
|
-
// #2973 review (Prism P1): a persistent pane is bound to a chat connection; changing it (a model change restarts it)
|
|
3570
|
-
// needs that connection's admins as well as the primary General's, exactly as its creation did. An instance whose
|
|
3571
|
-
// binding names no single configured connection is refused (fail closed), never confirmed by the General alone.
|
|
3572
|
-
const connections = [];
|
|
3573
|
-
if (isNativePaneInstance(inst) || inst.channel_id !== undefined) {
|
|
3574
|
-
const bound = inst.channel_id, channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
3575
|
-
if (typeof bound !== "string" || channels.filter(ch => (ch.id ?? ch.type) === bound).length !== 1)
|
|
3576
|
-
throw new Error("This Windows agent's chat connection cannot be resolved; change its settings in the web dashboard");
|
|
3577
|
-
connections.push(bound);
|
|
3578
|
-
}
|
|
3579
|
-
return this.proposeAgentWindowsChange({ requester, key: `${name}:${settingsFingerprint(edit)}`, fingerprint: settingsFingerprint([name, edit]),
|
|
3580
|
-
summary, machineId, bytes: JSON.stringify(edit).length, authority: { connections, primaryGeneral: true, unknown: false },
|
|
3581
|
-
// #2973 review (Prism P2): the update may wait for its lifecycle turn; if the agent is rebound meanwhile (B → C), the
|
|
3582
|
-
// approval for B must not stop or change it on C. Pin the approved target and require the execution's snapshot to be
|
|
3583
|
-
// unchanged before every pre-commit effect; the post-commit restart keeps its own (refreshed) checks.
|
|
3584
|
-
apply: (allowed, execution) => {
|
|
3585
|
-
const pinned = { channel: inst.channel_id, machine: inst.machine_id, adapter: inst.native_adapter, runtime: inst.runtime };
|
|
3586
|
-
const sameTarget = () => {
|
|
3587
|
-
const now = this.fleetConfig?.instances[name];
|
|
3588
|
-
return !!now && now.channel_id === pinned.channel && now.machine_id === pinned.machine && now.native_adapter === pinned.adapter && now.runtime === pinned.runtime;
|
|
3589
|
-
};
|
|
3590
|
-
// Before the commit: the exact target and an unchanged snapshot. After runtime.update completes the transaction,
|
|
3591
|
-
// current() is false by design; the committed operation's restart receipt then needs the same actor, target and
|
|
3592
|
-
// switch checks plus `completed` — a completed execution can never authorize another write (mutate refuses).
|
|
3593
|
-
return updateNativeWindowsInstance(this, name, edit, () => allowed() && sameTarget() && (execution.completed || execution.current()), execution);
|
|
3594
|
-
} });
|
|
3595
|
-
}
|
|
3596
1816
|
async promptSettingsChange(store, view) {
|
|
3597
1817
|
const generation = this.settingsGeneration;
|
|
3598
1818
|
const authority = this.settingsChatAuthority(store, view.id);
|
|
@@ -3601,15 +1821,25 @@ export class FleetManager {
|
|
|
3601
1821
|
return;
|
|
3602
1822
|
}
|
|
3603
1823
|
const excluded = new Set(store.affectedConnections(view.id));
|
|
3604
|
-
for (const name of Object.
|
|
3605
|
-
|
|
3606
|
-
|
|
1824
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
1825
|
+
if (!config.general_topic)
|
|
1826
|
+
continue;
|
|
1827
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1828
|
+
const general = this.daemons.get(name);
|
|
1829
|
+
if (!adapterId || !adapter || !general || excluded.has(adapterId) || adapter.type !== authority.platform)
|
|
1830
|
+
continue;
|
|
1831
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1832
|
+
if (!chatId || !topic)
|
|
3607
1833
|
continue;
|
|
3608
|
-
const { adapterId, adapter, chatId, topic } = route;
|
|
3609
1834
|
let retired = false, attached = false;
|
|
3610
1835
|
const postingDeadline = performance.now() + Math.min(5000, view.remaining_ms);
|
|
3611
|
-
const current = () => !retired && (attached || performance.now() < postingDeadline) && this.settingsGeneration === generation
|
|
3612
|
-
&&
|
|
1836
|
+
const current = () => !retired && (attached || performance.now() < postingDeadline) && !this.shuttingDown && this.settingsGeneration === generation
|
|
1837
|
+
&& this.fleetConfig?.instances[name]?.general_topic === true
|
|
1838
|
+
&& this.daemons.get(name) === general && !this.ipcStoppingInstances.has(name)
|
|
1839
|
+
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
1840
|
+
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
1841
|
+
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic
|
|
1842
|
+
&& authority.current();
|
|
3613
1843
|
if (!current())
|
|
3614
1844
|
continue;
|
|
3615
1845
|
try {
|
|
@@ -3619,7 +1849,7 @@ export class FleetManager {
|
|
|
3619
1849
|
t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
|
|
3620
1850
|
choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
|
|
3621
1851
|
expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
|
|
3622
|
-
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId)
|
|
1852
|
+
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
|
|
3623
1853
|
});
|
|
3624
1854
|
void posted.then(nonce => {
|
|
3625
1855
|
if (!retired)
|
|
@@ -3887,7 +2117,6 @@ export class FleetManager {
|
|
|
3887
2117
|
try {
|
|
3888
2118
|
const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
|
|
3889
2119
|
label: t("settings_confirm.admin_label", data.userId ?? ""),
|
|
3890
|
-
followStop: entry.confirmationStop,
|
|
3891
2120
|
current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
|
|
3892
2121
|
&& !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
|
|
3893
2122
|
});
|
|
@@ -3922,8 +2151,6 @@ export class FleetManager {
|
|
|
3922
2151
|
sseHeartbeatMs = SSE_HEARTBEAT_MS;
|
|
3923
2152
|
/** The dashboard's login codes. Memory only: a code that outlives the process is a code nobody can prove was not copied. */
|
|
3924
2153
|
webLoginCodes = null;
|
|
3925
|
-
/** Refs #3061 (P2a): device-code sign-in requests (in memory; web-device-login.ts). Read by auth-api as `webDeviceLogins`. */
|
|
3926
|
-
webDeviceLogins = null;
|
|
3927
2154
|
/**
|
|
3928
2155
|
* #1920 step 4: guests' codes, apart from the admins' — one per account, so no guest's `/web` withdraws another's or
|
|
3929
2156
|
* an admin's code (#2093 review), at most 32 at once. Local sign-in only for now (audience "local").
|
|
@@ -4281,15 +2508,12 @@ export class FleetManager {
|
|
|
4281
2508
|
}
|
|
4282
2509
|
/** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
|
|
4283
2510
|
admitDurableDelivery(input) {
|
|
4284
|
-
const bindingHold = this.generalLocationDeliveryHold(input.targetInstance);
|
|
4285
|
-
if (bindingHold)
|
|
4286
|
-
throw Error(bindingHold);
|
|
4287
2511
|
const outbox = this.deliveryOutbox;
|
|
4288
2512
|
if (!outbox)
|
|
4289
2513
|
throw new Error("Durable delivery store is unavailable");
|
|
4290
|
-
const
|
|
4291
|
-
const sourceDaemonBootId = input.sourceDaemonBootId ??
|
|
4292
|
-
if (!
|
|
2514
|
+
const source = this.daemons.get(input.sourceInstance);
|
|
2515
|
+
const sourceDaemonBootId = input.sourceDaemonBootId ?? source?.bootId;
|
|
2516
|
+
if (!source || !sourceDaemonBootId || source.bootId !== sourceDaemonBootId) {
|
|
4293
2517
|
throw new Error("Source daemon generation is no longer current; outcome is unknown");
|
|
4294
2518
|
}
|
|
4295
2519
|
const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
|
|
@@ -4323,7 +2547,7 @@ export class FleetManager {
|
|
|
4323
2547
|
const human = (item) => {
|
|
4324
2548
|
if (!item.authority || !state)
|
|
4325
2549
|
return item;
|
|
4326
|
-
const authority =
|
|
2550
|
+
const authority = authorityNow(item.authority, state);
|
|
4327
2551
|
return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
|
|
4328
2552
|
};
|
|
4329
2553
|
// #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
|
|
@@ -4352,11 +2576,6 @@ export class FleetManager {
|
|
|
4352
2576
|
return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
|
|
4353
2577
|
}
|
|
4354
2578
|
getDaemonBootId(instanceName) {
|
|
4355
|
-
if (this.generalLocationDeliveryHold(instanceName))
|
|
4356
|
-
return undefined;
|
|
4357
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
4358
|
-
return !this.shuttingDown && windowsCliEnabled() && this.nativeMcpStore?.hasActive(instanceName) ? this.managerBootId : undefined;
|
|
4359
|
-
}
|
|
4360
2579
|
return this.daemons.get(instanceName)?.bootId;
|
|
4361
2580
|
}
|
|
4362
2581
|
markDurableResponseDelivered(sourceInstance, operationId) {
|
|
@@ -4365,22 +2584,8 @@ export class FleetManager {
|
|
|
4365
2584
|
/** Called after a replacement Daemon object is installed in lifecycle.daemons. */
|
|
4366
2585
|
onDaemonReady(name, daemonBootId) {
|
|
4367
2586
|
const daemon = this.daemons.get(name);
|
|
4368
|
-
if (daemon?.bootId === daemonBootId)
|
|
2587
|
+
if (daemon?.bootId === daemonBootId)
|
|
4369
2588
|
this.listenForWorkingState(name, daemon);
|
|
4370
|
-
const cfg = this.fleetConfig?.instances[name];
|
|
4371
|
-
if (cfg?.general_topic && !this.generalLocationFlights.has(name) && daemon.getProcessStatus() === "running"
|
|
4372
|
-
&& (!isNativePaneInstance(cfg) || daemon.nativePaneCurrent())) {
|
|
4373
|
-
try {
|
|
4374
|
-
if (this.generalBindings().completeSelected(name, cfg)) {
|
|
4375
|
-
this.needsYou?.poke();
|
|
4376
|
-
this.logger.info({ name }, "General location's selected start receipt settled after recovery");
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
catch (err) {
|
|
4380
|
-
this.logger.warn({ name, err }, "General location start receipt remains held");
|
|
4381
|
-
}
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
2589
|
// #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
|
|
4385
2590
|
if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
|
|
4386
2591
|
void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
|
|
@@ -4528,15 +2733,6 @@ export class FleetManager {
|
|
|
4528
2733
|
this.deliveryPumpRunning = true;
|
|
4529
2734
|
this.deliveryPumpScheduled = false;
|
|
4530
2735
|
try {
|
|
4531
|
-
// Native workers have no resident daemon to wake. Pending durable work resumes their logical instance;
|
|
4532
|
-
// the Windows bridge still owns the single physical execution slot.
|
|
4533
|
-
for (const target of new Set(outbox.listPending().map(row => row.targetInstance))) {
|
|
4534
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[target])) {
|
|
4535
|
-
const runtime = this.nativeInstanceRuntime(), agent = runtime.get(target);
|
|
4536
|
-
if (agent?.paused && !agent.blocked)
|
|
4537
|
-
runtime.setPaused(target, false);
|
|
4538
|
-
}
|
|
4539
|
-
}
|
|
4540
2736
|
outbox.expireStale(undefined, undefined, target => {
|
|
4541
2737
|
const failure = this.wakeCoordinator?.wakeFailure(target);
|
|
4542
2738
|
return failure ? `target could not be woken (${failure})` : undefined;
|
|
@@ -4549,7 +2745,7 @@ export class FleetManager {
|
|
|
4549
2745
|
// Phase 2c: a lane a queue worker owns is never claimed here.
|
|
4550
2746
|
target => this.queueWorkers.has(target) || this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target)
|
|
4551
2747
|
? null
|
|
4552
|
-
: this.
|
|
2748
|
+
: this.daemons.get(target)?.bootId ?? null, this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
|
|
4553
2749
|
if (!claimed)
|
|
4554
2750
|
break;
|
|
4555
2751
|
this.activeDurableTargets.add(claimed.targetInstance);
|
|
@@ -4610,7 +2806,7 @@ export class FleetManager {
|
|
|
4610
2806
|
wanted: () => this.deliveryWorkerMode(target) === "on",
|
|
4611
2807
|
available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
|
|
4612
2808
|
blocked: () => this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target) === true,
|
|
4613
|
-
daemonBootId: () => this.
|
|
2809
|
+
daemonBootId: () => this.daemons.get(target)?.bootId ?? null,
|
|
4614
2810
|
claim: bootId => this.deliveryOutbox?.claimNext(this.managerBootId, candidate => candidate === target ? bootId : null, new Set(), row => this.recoveryDeliveryEligible(row)),
|
|
4615
2811
|
tryAcquireBudget: () => {
|
|
4616
2812
|
if (this.activeDurableTargets.size >= 8)
|
|
@@ -4641,10 +2837,6 @@ export class FleetManager {
|
|
|
4641
2837
|
const outbox = this.deliveryOutbox;
|
|
4642
2838
|
if (!outbox)
|
|
4643
2839
|
return;
|
|
4644
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[claimed.targetInstance])) {
|
|
4645
|
-
await this.dispatchNativeDurableDelivery(claimed);
|
|
4646
|
-
return;
|
|
4647
|
-
}
|
|
4648
2840
|
const target = claimed.targetInstance;
|
|
4649
2841
|
const attempt = claimed.attemptNo;
|
|
4650
2842
|
const payload = { ...claimed.payload };
|
|
@@ -4726,85 +2918,6 @@ export class FleetManager {
|
|
|
4726
2918
|
}
|
|
4727
2919
|
await laneReleased;
|
|
4728
2920
|
}
|
|
4729
|
-
/** The ordinary durable lane, with provider completion proof instead of terminal/IPC submission proof. */
|
|
4730
|
-
async dispatchNativeDurableDelivery(claimed) {
|
|
4731
|
-
const outbox = this.deliveryOutbox;
|
|
4732
|
-
if (!outbox)
|
|
4733
|
-
return;
|
|
4734
|
-
const { deliveryId, targetDaemonBootId, attemptNo, targetInstance: target } = claimed;
|
|
4735
|
-
let begun = false;
|
|
4736
|
-
try {
|
|
4737
|
-
if (["raw_paste", "steer"].includes(claimed.kind)) {
|
|
4738
|
-
if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) === "begun")
|
|
4739
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "failed", "Windows runtime does not support terminal input or steering");
|
|
4740
|
-
return;
|
|
4741
|
-
}
|
|
4742
|
-
const meta = claimed.payload.meta && typeof claimed.payload.meta === "object" ? claimed.payload.meta : {};
|
|
4743
|
-
const task = await this.nativeInstanceRuntime().execute(target, { ...claimed.payload, requestId: deliveryId, meta: { ...meta, from_instance: claimed.sourceInstance, correlation_id: claimed.correlationId } }, {
|
|
4744
|
-
stillCurrent: () => !this.shuttingDown,
|
|
4745
|
-
beforeStart: () => { if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) !== "begun")
|
|
4746
|
-
throw new Error("Windows delivery attempt is no longer current"); begun = true; },
|
|
4747
|
-
});
|
|
4748
|
-
const outcome = task.outcomeConfirmed === false ? "uncertain" : task.status === "completed" ? "delivered" : "failed";
|
|
4749
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, outcome, task.error || task.status);
|
|
4750
|
-
// Return a requested peer report when the provider did not explicitly report it through scoped MCP.
|
|
4751
|
-
if (meta.requires_reply === "true" && claimed.correlationId && !task.reportedCorrelationIds?.includes(claimed.correlationId) && this.fleetConfig?.instances[claimed.sourceInstance]) {
|
|
4752
|
-
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
4753
|
-
outbox.admit({ sourceKey: `native-result:${deliveryId}`, operationId: `native-result:${deliveryId}`, sourceInstance: target, sourceDaemonBootId: this.managerBootId,
|
|
4754
|
-
targetInstance: claimed.sourceInstance, kind: "fleet_inbound", correlationId: claimed.correlationId,
|
|
4755
|
-
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" } } });
|
|
4756
|
-
this.scheduleDeliveryOutboxPump();
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
catch (error) {
|
|
4760
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4761
|
-
if (begun)
|
|
4762
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "uncertain", reason);
|
|
4763
|
-
else
|
|
4764
|
-
outbox.retryBeforeBegin(deliveryId, targetDaemonBootId, attemptNo, reason, 5000);
|
|
4765
|
-
this.logger.warn({ target, deliveryId, err: reason }, "Windows durable delivery did not complete");
|
|
4766
|
-
}
|
|
4767
|
-
}
|
|
4768
|
-
/** A runtime's persisted completion can outlive this manager. Resolve its original delivery fence, never send
|
|
4769
|
-
* the prompt again, and reconstruct a missing peer report using the same durable operation ID. */
|
|
4770
|
-
reconcileRemoteDelivery(machineId, task) {
|
|
4771
|
-
const outbox = this.deliveryOutbox;
|
|
4772
|
-
if (!outbox?.isOpen || !task.finishedAt || task.outcomeConfirmed !== true || !["completed", "failed", "cancelled"].includes(task.status))
|
|
4773
|
-
return;
|
|
4774
|
-
const key = `${task.id}:${task.finishedAt}:${task.status}`;
|
|
4775
|
-
if (this.reconciledRemoteResults.has(key))
|
|
4776
|
-
return;
|
|
4777
|
-
const row = outbox.get(task.id);
|
|
4778
|
-
if (!row || !isNativeExecInstance(this.fleetConfig?.instances[row.targetInstance]) || this.fleetConfig?.instances[row.targetInstance]?.machine_id !== machineId
|
|
4779
|
-
|| !row.targetDaemonBootId || this.activeDurableTargets.has(row.targetInstance))
|
|
4780
|
-
return;
|
|
4781
|
-
if (row.state === "submission_started" || row.state === "reconciliation_pending") {
|
|
4782
|
-
if (!outbox.complete(row.deliveryId, row.targetDaemonBootId, row.attemptNo, task.status === "completed" ? "delivered" : "failed", task.error || "Runtime completion recovered after reconnect"))
|
|
4783
|
-
return;
|
|
4784
|
-
}
|
|
4785
|
-
else if (row.state === "uncertain" && task.status === "completed") {
|
|
4786
|
-
if (outbox.markConsumed(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "turn", "Paired runtime journal confirmed this exact execution completed") === "ignored")
|
|
4787
|
-
return;
|
|
4788
|
-
}
|
|
4789
|
-
else if (row.state !== "delivered" && row.state !== "failed")
|
|
4790
|
-
return;
|
|
4791
|
-
const meta = row.payload.meta && typeof row.payload.meta === "object" ? row.payload.meta : {};
|
|
4792
|
-
if (meta.requires_reply !== "true" || !row.correlationId || !this.fleetConfig?.instances[row.sourceInstance]) {
|
|
4793
|
-
this.reconciledRemoteResults.add(key);
|
|
4794
|
-
return;
|
|
4795
|
-
}
|
|
4796
|
-
const reports = outbox.queryStatusForInstance(row.targetInstance, { correlationId: row.correlationId, limit: 100 }).items;
|
|
4797
|
-
if (reports.some(report => report.source_instance === row.targetInstance && report.target_instance === row.sourceInstance
|
|
4798
|
-
&& report.kind === "report" && !["failed", "cancelled"].includes(report.state))) {
|
|
4799
|
-
this.reconciledRemoteResults.add(key);
|
|
4800
|
-
return;
|
|
4801
|
-
}
|
|
4802
|
-
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
4803
|
-
outbox.admit({ sourceKey: `native-result:${row.deliveryId}`, operationId: `native-result:${row.deliveryId}`, sourceInstance: row.targetInstance, sourceDaemonBootId: this.managerBootId,
|
|
4804
|
-
targetInstance: row.sourceInstance, kind: "fleet_inbound", correlationId: row.correlationId,
|
|
4805
|
-
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" } } });
|
|
4806
|
-
this.reconciledRemoteResults.add(key);
|
|
4807
|
-
}
|
|
4808
2921
|
async waitForDurableLaneRelease(claimed) {
|
|
4809
2922
|
const outbox = this.deliveryOutbox;
|
|
4810
2923
|
if (!outbox)
|
|
@@ -5261,8 +3374,6 @@ export class FleetManager {
|
|
|
5261
3374
|
this.warnAboutRemovedLoginMode(loaded);
|
|
5262
3375
|
this.enforceGuestSwitch();
|
|
5263
3376
|
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
5264
|
-
this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
|
|
5265
|
-
this.observeHubSwitches(); // S-51: and the Hub's (no-op until the fleet has started)
|
|
5266
3377
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
5267
3378
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
5268
3379
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -5316,7 +3427,6 @@ export class FleetManager {
|
|
|
5316
3427
|
* guild channel.
|
|
5317
3428
|
*/
|
|
5318
3429
|
reregisterClassicChannels() {
|
|
5319
|
-
this.nativeMcpStore?.observePolicies();
|
|
5320
3430
|
this.agentCredentialsCache?.observePolicies();
|
|
5321
3431
|
if (!this.classicChannels)
|
|
5322
3432
|
return;
|
|
@@ -5370,9 +3480,13 @@ export class FleetManager {
|
|
|
5370
3480
|
}
|
|
5371
3481
|
/** AgEnD package version (for the Settings "current version" / What's New). */
|
|
5372
3482
|
get currentVersion() {
|
|
5373
|
-
|
|
3483
|
+
try {
|
|
3484
|
+
return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version ?? "unknown";
|
|
3485
|
+
}
|
|
3486
|
+
catch {
|
|
3487
|
+
return "unknown";
|
|
3488
|
+
}
|
|
5374
3489
|
}
|
|
5375
|
-
get currentBuild() { return RUNNING_BUILD; }
|
|
5376
3490
|
/**
|
|
5377
3491
|
* Resolve a slash-command target in a channel. Classic channels are looked up
|
|
5378
3492
|
* per-bot (same-channel multi-bot); a fleet-topic instance is found via the
|
|
@@ -5689,8 +3803,6 @@ export class FleetManager {
|
|
|
5689
3803
|
}
|
|
5690
3804
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
5691
3805
|
getAdapterForInstance(name) {
|
|
5692
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5693
|
-
return null;
|
|
5694
3806
|
const worldId = this.getInstanceAdapterId(name);
|
|
5695
3807
|
if (worldId)
|
|
5696
3808
|
return this.worlds.get(worldId)?.adapter ?? this.adapter;
|
|
@@ -5753,8 +3865,6 @@ export class FleetManager {
|
|
|
5753
3865
|
}
|
|
5754
3866
|
/** Get the group_id for an instance's bound adapter */
|
|
5755
3867
|
getGroupIdForInstance(name) {
|
|
5756
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5757
|
-
return "";
|
|
5758
3868
|
const adapterId = this.getInstanceAdapterId(name);
|
|
5759
3869
|
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
5760
3870
|
return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
@@ -5791,8 +3901,6 @@ export class FleetManager {
|
|
|
5791
3901
|
*/
|
|
5792
3902
|
getInstanceAdapterId(name) {
|
|
5793
3903
|
const cfg = this.fleetConfig?.instances[name];
|
|
5794
|
-
if (isNativeExecInstance(cfg))
|
|
5795
|
-
return undefined;
|
|
5796
3904
|
if (cfg)
|
|
5797
3905
|
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
5798
3906
|
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
|
|
@@ -5820,32 +3928,9 @@ export class FleetManager {
|
|
|
5820
3928
|
this.instanceWorldBinding.set(name, adapterId);
|
|
5821
3929
|
}
|
|
5822
3930
|
getInstanceStatus(name) {
|
|
5823
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5824
|
-
try {
|
|
5825
|
-
return this.nativeInstanceRuntime().status(name);
|
|
5826
|
-
}
|
|
5827
|
-
catch {
|
|
5828
|
-
return "crashed";
|
|
5829
|
-
}
|
|
5830
|
-
}
|
|
5831
3931
|
if (this.lifecycle.isPaused(name))
|
|
5832
3932
|
return "paused";
|
|
5833
3933
|
const daemon = this.lifecycle.daemons.get(name);
|
|
5834
|
-
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
5835
|
-
const config = this.fleetConfig.instances[name];
|
|
5836
|
-
if (config.machine_id ? !remoteRuntimeEnabled() : !localWindowsCliEnabled())
|
|
5837
|
-
return "stopped";
|
|
5838
|
-
if (daemon) {
|
|
5839
|
-
const native = daemon, status = native.getProcessStatus?.() ?? "crashed";
|
|
5840
|
-
return status === "running" && native.nativePaneCurrent?.() !== true ? "crashed" : status;
|
|
5841
|
-
}
|
|
5842
|
-
try {
|
|
5843
|
-
return this.nativeInstanceRuntime().get(name)?.activeId ? "crashed" : "stopped";
|
|
5844
|
-
}
|
|
5845
|
-
catch {
|
|
5846
|
-
return "crashed";
|
|
5847
|
-
}
|
|
5848
|
-
}
|
|
5849
3934
|
const processStatus = this.instanceProcessStatus.get(name);
|
|
5850
3935
|
// IPC can be disconnected during a respawn, so the event which announces
|
|
5851
3936
|
// the new live pane may be missed. The in-process daemon is authoritative
|
|
@@ -5879,14 +3964,6 @@ export class FleetManager {
|
|
|
5879
3964
|
}
|
|
5880
3965
|
}
|
|
5881
3966
|
getInstanceExecutionState(name) {
|
|
5882
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5883
|
-
try {
|
|
5884
|
-
return this.nativeInstanceRuntime().execution(name);
|
|
5885
|
-
}
|
|
5886
|
-
catch {
|
|
5887
|
-
return null;
|
|
5888
|
-
}
|
|
5889
|
-
}
|
|
5890
3967
|
if (this.lifecycle.isPaused(name))
|
|
5891
3968
|
return null;
|
|
5892
3969
|
// Process status wins over a stale pane snapshot. A dead remain-on-exit pane
|
|
@@ -5897,8 +3974,6 @@ export class FleetManager {
|
|
|
5897
3974
|
}
|
|
5898
3975
|
/** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
|
|
5899
3976
|
getInstanceInteraction(name) {
|
|
5900
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5901
|
-
return null;
|
|
5902
3977
|
if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
|
|
5903
3978
|
return null;
|
|
5904
3979
|
const daemon = this.lifecycle.daemons.get(name);
|
|
@@ -5986,8 +4061,6 @@ export class FleetManager {
|
|
|
5986
4061
|
bindings.push([name, backend, profile]);
|
|
5987
4062
|
};
|
|
5988
4063
|
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
5989
|
-
if (isWindowsInstance(config))
|
|
5990
|
-
continue; // Windows sign-ins and usage do not belong to the Linux account.
|
|
5991
4064
|
// loadFleetConfig() has already merged the fleet default into each row.
|
|
5992
4065
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5993
4066
|
add(name, backend, instanceCredentialProfile(config, this.fleetConfig?.defaults, backend));
|
|
@@ -6318,7 +4391,7 @@ export class FleetManager {
|
|
|
6318
4391
|
finish(false);
|
|
6319
4392
|
});
|
|
6320
4393
|
}
|
|
6321
|
-
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold
|
|
4394
|
+
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold) {
|
|
6322
4395
|
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
6323
4396
|
return false;
|
|
6324
4397
|
await this.holdDeliveryForStorm(instanceName, deliveryEpoch);
|
|
@@ -6357,8 +4430,7 @@ export class FleetManager {
|
|
|
6357
4430
|
const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
|
|
6358
4431
|
this.logger[log.level](log.fields, log.message);
|
|
6359
4432
|
}
|
|
6360
|
-
|
|
6361
|
-
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
|
|
4433
|
+
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
|
|
6362
4434
|
if (!sent)
|
|
6363
4435
|
return false;
|
|
6364
4436
|
if (this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent)) {
|
|
@@ -6391,25 +4463,21 @@ export class FleetManager {
|
|
|
6391
4463
|
* `onStartRetryHold` (#2484, a person's message): an instance in its own start-retry backoff is waited for until that
|
|
6392
4464
|
* retry brings it up, however long the backoff, and fails only once the retries give up.
|
|
6393
4465
|
*/
|
|
6394
|
-
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold
|
|
4466
|
+
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
6395
4467
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
6396
4468
|
return false;
|
|
6397
4469
|
const queued = this.ipcWaitTails.get(instanceName);
|
|
6398
4470
|
if (!queued) {
|
|
6399
4471
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6400
|
-
if (ipc?.connected)
|
|
6401
|
-
|
|
6402
|
-
return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
|
|
6403
|
-
if (ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
6404
|
-
return true;
|
|
6405
|
-
}
|
|
4472
|
+
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
4473
|
+
return true;
|
|
6406
4474
|
}
|
|
6407
4475
|
const attempt = (queued ?? Promise.resolve())
|
|
6408
4476
|
.catch(() => { })
|
|
6409
4477
|
.then(() => {
|
|
6410
4478
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
6411
4479
|
return false;
|
|
6412
|
-
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold
|
|
4480
|
+
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
|
|
6413
4481
|
});
|
|
6414
4482
|
// The chain stores a settled-either-way promise so one failed delivery cannot
|
|
6415
4483
|
// wedge every later one, and so `queued` above is safe to await unguarded.
|
|
@@ -6426,21 +4494,8 @@ export class FleetManager {
|
|
|
6426
4494
|
}
|
|
6427
4495
|
}
|
|
6428
4496
|
}
|
|
6429
|
-
/**
|
|
6430
|
-
* The Hub representative's hand-off (#3145): only once a connected client exists, after every reconnect/order wait,
|
|
6431
|
-
* the begin lease + input intent (beforeInput), then the fence and the LIVE client are checked again and the payload
|
|
6432
|
-
* goes in one attempt. After the intent is spent nothing here retries: false is the caller's uncertain outcome.
|
|
6433
|
-
*/
|
|
6434
|
-
async handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput) {
|
|
6435
|
-
if (!(await beforeInput()))
|
|
6436
|
-
return false;
|
|
6437
|
-
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
6438
|
-
return false;
|
|
6439
|
-
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6440
|
-
return !!ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload));
|
|
6441
|
-
}
|
|
6442
4497
|
/** Poll for the instance's IPC to come back, then send. Throws if it does not. */
|
|
6443
|
-
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold
|
|
4498
|
+
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
6444
4499
|
let deadline = Date.now() + IPC_RECONNECT_GRACE_MS;
|
|
6445
4500
|
let warned = false;
|
|
6446
4501
|
let held = false;
|
|
@@ -6450,8 +4505,6 @@ export class FleetManager {
|
|
|
6450
4505
|
// Re-read every round: a reconnect replaces the IpcClient object entirely,
|
|
6451
4506
|
// so a cached reference would stay dead forever.
|
|
6452
4507
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6453
|
-
if (ipc?.connected && beforeInput)
|
|
6454
|
-
return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
|
|
6455
4508
|
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
6456
4509
|
return true;
|
|
6457
4510
|
// #2484: while the instance waits for (or runs) its own start retry, the grace runs from now — so the message is
|
|
@@ -6621,8 +4674,6 @@ export class FleetManager {
|
|
|
6621
4674
|
* id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
|
|
6622
4675
|
*/
|
|
6623
4676
|
startWebChatProgress(instanceName) {
|
|
6624
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
6625
|
-
return Promise.resolve();
|
|
6626
4677
|
if (this.shuttingDown)
|
|
6627
4678
|
return Promise.resolve();
|
|
6628
4679
|
return this.sendCancelButton(instanceName);
|
|
@@ -6888,14 +4939,6 @@ export class FleetManager {
|
|
|
6888
4939
|
}
|
|
6889
4940
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
6890
4941
|
async deliverToInstance(instanceName, payload, options = {}) {
|
|
6891
|
-
if (this.generalLocationDeliveryHold(instanceName))
|
|
6892
|
-
return false;
|
|
6893
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
6894
|
-
if (options.beforeInput)
|
|
6895
|
-
return false; // no pre-input hook on the Windows runtime
|
|
6896
|
-
return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
|
|
6897
|
-
...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
|
|
6898
|
-
}
|
|
6899
4942
|
const deliveryEpoch = this.getDeliveryEpoch(instanceName);
|
|
6900
4943
|
const deliveryPayload = { ...payload, delivery_epoch: deliveryEpoch };
|
|
6901
4944
|
const meta = payload.meta && typeof payload.meta === "object"
|
|
@@ -6917,7 +4960,7 @@ export class FleetManager {
|
|
|
6917
4960
|
return false;
|
|
6918
4961
|
this.enforceWarmCap(instanceName); // woke one → evict a different LRU idle if over cap
|
|
6919
4962
|
}
|
|
6920
|
-
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold
|
|
4963
|
+
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold);
|
|
6921
4964
|
if (!sent)
|
|
6922
4965
|
return false;
|
|
6923
4966
|
// A cross-instance item arriving before the daemon observes this turn as
|
|
@@ -6928,7 +4971,7 @@ export class FleetManager {
|
|
|
6928
4971
|
return true;
|
|
6929
4972
|
}
|
|
6930
4973
|
const previous = this.idleGatedDeliveryTails.get(instanceName) ?? Promise.resolve();
|
|
6931
|
-
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold
|
|
4974
|
+
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold));
|
|
6932
4975
|
this.idleGatedDeliveryTails.set(instanceName, delivery);
|
|
6933
4976
|
try {
|
|
6934
4977
|
return await delivery;
|
|
@@ -7453,10 +5496,6 @@ export class FleetManager {
|
|
|
7453
5496
|
* slot admission and joins any wake already in flight for the target.
|
|
7454
5497
|
*/
|
|
7455
5498
|
async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
|
|
7456
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7457
|
-
this.nativeInstanceRuntime().setPaused(name, false);
|
|
7458
|
-
return;
|
|
7459
|
-
}
|
|
7460
5499
|
if (this.sessionOomStopped(name)) {
|
|
7461
5500
|
if (!retryStoppedSession)
|
|
7462
5501
|
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
@@ -7481,10 +5520,6 @@ export class FleetManager {
|
|
|
7481
5520
|
.sort((a, b) => (readLastInboundAt(this.getInstanceDir(a)) ?? 0) - (readLastInboundAt(this.getInstanceDir(b)) ?? 0))[0];
|
|
7482
5521
|
}
|
|
7483
5522
|
async changeInstancePauseState(name, action) {
|
|
7484
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7485
|
-
this.nativeInstanceRuntime().setPaused(name, action === "pause");
|
|
7486
|
-
return action === "pause" ? "paused" : "awake";
|
|
7487
|
-
}
|
|
7488
5523
|
if (action === "wake") {
|
|
7489
5524
|
await this.explicitWake(name, 30_000, true);
|
|
7490
5525
|
this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
|
|
@@ -7511,6 +5546,7 @@ export class FleetManager {
|
|
|
7511
5546
|
}
|
|
7512
5547
|
classicBehaviorUpdate(instanceName) {
|
|
7513
5548
|
return {
|
|
5549
|
+
quota_notice: this.classicChannels?.getQuotaNoticeByInstance?.(instanceName) ?? null,
|
|
7514
5550
|
reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
|
|
7515
5551
|
tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
|
|
7516
5552
|
reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
|
|
@@ -7662,14 +5698,6 @@ export class FleetManager {
|
|
|
7662
5698
|
resumePaused = false,
|
|
7663
5699
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
7664
5700
|
transition, execution, kiroMigration) {
|
|
7665
|
-
if (isNativeExecInstance(config)) {
|
|
7666
|
-
execution?.assert();
|
|
7667
|
-
if (!windowsCliEnabled())
|
|
7668
|
-
throw new Error("Windows runtime is disabled");
|
|
7669
|
-
if (resumePaused)
|
|
7670
|
-
this.nativeInstanceRuntime().setPaused(name, false);
|
|
7671
|
-
return;
|
|
7672
|
-
}
|
|
7673
5701
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
7674
5702
|
if (migration && migration.token !== kiroMigration)
|
|
7675
5703
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -7720,29 +5748,27 @@ export class FleetManager {
|
|
|
7720
5748
|
if (resumePaused)
|
|
7721
5749
|
this.lifecycle.markActivitySeedNow(name);
|
|
7722
5750
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
7723
|
-
// #1490 row 67
|
|
5751
|
+
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
7724
5752
|
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
7725
5753
|
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
7726
5754
|
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
7727
5755
|
// accept the stale start. Lifecycle start looks up again before its own
|
|
7728
|
-
// createBackend; the probe cache makes that cheap.
|
|
7729
|
-
// agent's CLI is on that PC — nothing to resolve on this one.)
|
|
5756
|
+
// createBackend; the probe cache makes that cheap.
|
|
7730
5757
|
const epoch = this.lifecycle.epochOf(name);
|
|
7731
5758
|
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
7732
|
-
const
|
|
7733
|
-
const resolved = windows ? null : await resolveBackendBinary(backend);
|
|
5759
|
+
const resolved = await resolveBackendBinary(backend);
|
|
7734
5760
|
execution?.assert();
|
|
7735
5761
|
if (this.lifecycle.epochOf(name) !== epoch
|
|
7736
5762
|
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
7737
5763
|
throw new SupersededStartError(name);
|
|
7738
5764
|
}
|
|
7739
5765
|
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
7740
|
-
if (binary
|
|
5766
|
+
if (binary)
|
|
7741
5767
|
noteResolvedBinary(binary, resolved ?? binary);
|
|
7742
|
-
if (
|
|
5768
|
+
if (config.general_topic) {
|
|
7743
5769
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
7744
5770
|
}
|
|
7745
|
-
else if (
|
|
5771
|
+
else if (kind === "fleet-topic") {
|
|
7746
5772
|
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
7747
5773
|
// deliberately excluded: their workspace and conversation lifecycle are
|
|
7748
5774
|
// managed independently from fleet-topic workers.
|
|
@@ -7757,7 +5783,7 @@ export class FleetManager {
|
|
|
7757
5783
|
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
7758
5784
|
}
|
|
7759
5785
|
}
|
|
7760
|
-
else if (
|
|
5786
|
+
else if (kind === "classic") {
|
|
7761
5787
|
// #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
|
|
7762
5788
|
// coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
|
|
7763
5789
|
// room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
|
|
@@ -7774,7 +5800,7 @@ export class FleetManager {
|
|
|
7774
5800
|
}
|
|
7775
5801
|
}
|
|
7776
5802
|
const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
|
|
7777
|
-
const identity = { kind, backend, model:
|
|
5803
|
+
const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
|
|
7778
5804
|
if (receipt) {
|
|
7779
5805
|
await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
|
|
7780
5806
|
// Only this transition's actual publication can acquire its replacement.
|
|
@@ -7844,7 +5870,7 @@ export class FleetManager {
|
|
|
7844
5870
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
7845
5871
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
7846
5872
|
// full stagger slot for every distinct working directory.
|
|
7847
|
-
const runnableEntries = entries.filter(([name
|
|
5873
|
+
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
7848
5874
|
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
7849
5875
|
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
7850
5876
|
const fleetNames = new Set(entries.map(([name]) => name));
|
|
@@ -7910,8 +5936,6 @@ export class FleetManager {
|
|
|
7910
5936
|
return false;
|
|
7911
5937
|
if (err instanceof SessionOomStoppedError)
|
|
7912
5938
|
await this.notifySessionOomStop(name);
|
|
7913
|
-
if (this.holdNativePaneRecovery(name, err))
|
|
7914
|
-
return false;
|
|
7915
5939
|
if (this.stopOnUnsupportedCli(name, err, what))
|
|
7916
5940
|
return false;
|
|
7917
5941
|
this.logger.error({ err, name }, `Failed to start ${what}`);
|
|
@@ -8053,8 +6077,6 @@ export class FleetManager {
|
|
|
8053
6077
|
if (err instanceof SupersededStartError)
|
|
8054
6078
|
return;
|
|
8055
6079
|
// An earlier attempt could not tell (unknown probe); this one could.
|
|
8056
|
-
if (this.holdNativePaneRecovery(name, err))
|
|
8057
|
-
return;
|
|
8058
6080
|
if (this.stopOnUnsupportedCli(name, err, "instance"))
|
|
8059
6081
|
return;
|
|
8060
6082
|
this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
|
|
@@ -8316,7 +6338,7 @@ export class FleetManager {
|
|
|
8316
6338
|
return count;
|
|
8317
6339
|
}
|
|
8318
6340
|
configuredStartupInstanceNames(fleet, includeClassic) {
|
|
8319
|
-
const names = new Set(Object.
|
|
6341
|
+
const names = new Set(Object.keys(fleet.instances));
|
|
8320
6342
|
if (includeClassic) {
|
|
8321
6343
|
for (const channel of this.classicChannels?.getAll() ?? [])
|
|
8322
6344
|
names.add(channel.instanceName);
|
|
@@ -8367,10 +6389,6 @@ export class FleetManager {
|
|
|
8367
6389
|
return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
|
|
8368
6390
|
}
|
|
8369
6391
|
async stopInstance(name, transition) {
|
|
8370
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
8371
|
-
this.nativeInstanceRuntime().setPaused(name, true);
|
|
8372
|
-
return { backgroundWorkEnded: false };
|
|
8373
|
-
}
|
|
8374
6392
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
8375
6393
|
this.cancelStartupRetry(name);
|
|
8376
6394
|
this.failoverActive.delete(name);
|
|
@@ -8410,16 +6428,6 @@ export class FleetManager {
|
|
|
8410
6428
|
async restartSingleInstance(name, opts) {
|
|
8411
6429
|
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
8412
6430
|
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
8413
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
8414
|
-
const runtime = this.nativeInstanceRuntime();
|
|
8415
|
-
if (runtime.get(name)?.blocked)
|
|
8416
|
-
throw new Error("Verify the disconnected Windows process stopped and recover this agent before restarting");
|
|
8417
|
-
runtime.cancel(name);
|
|
8418
|
-
runtime.setPaused(name, false);
|
|
8419
|
-
return;
|
|
8420
|
-
}
|
|
8421
|
-
if (opts?.explicit && isNativePaneInstance(this.fleetConfig?.instances[name]))
|
|
8422
|
-
this.lifecycle.resetNativeCrashBudget(name);
|
|
8423
6431
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
8424
6432
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
8425
6433
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -8483,7 +6491,7 @@ export class FleetManager {
|
|
|
8483
6491
|
return run;
|
|
8484
6492
|
}
|
|
8485
6493
|
isInstanceRestarting(name) {
|
|
8486
|
-
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true
|
|
6494
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
8487
6495
|
}
|
|
8488
6496
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
8489
6497
|
restartInFlight(name) {
|
|
@@ -8716,12 +6724,6 @@ export class FleetManager {
|
|
|
8716
6724
|
},
|
|
8717
6725
|
});
|
|
8718
6726
|
}
|
|
8719
|
-
if (!this.webDeviceLogins) {
|
|
8720
|
-
this.webDeviceLogins = new DeviceLoginRequests({
|
|
8721
|
-
onEvent: event => { if (event === "breaker-open")
|
|
8722
|
-
this.logger.warn("Device sign-in approval paused: too many wrong codes"); },
|
|
8723
|
-
});
|
|
8724
|
-
}
|
|
8725
6727
|
if (!this.webGuestLoginCodes) {
|
|
8726
6728
|
this.webGuestLoginCodes = new GuestLoginCodes({
|
|
8727
6729
|
onEvent: event => {
|
|
@@ -8784,9 +6786,6 @@ export class FleetManager {
|
|
|
8784
6786
|
log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
|
|
8785
6787
|
}
|
|
8786
6788
|
publicOwnerCurrent(owner) {
|
|
8787
|
-
// #3008: the outside link is this computer's admin's — while "Allow connections from outside" is on, web-only.
|
|
8788
|
-
if (isOutsideLinkOwner(owner))
|
|
8789
|
-
return !this.shuttingDown && this.outsideOn();
|
|
8790
6789
|
if (this.shuttingDown || !this.hasFleetAdmins(owner.adapterId) || !this.isFleetAdmin(owner.userId, owner.adapterId))
|
|
8791
6790
|
return false;
|
|
8792
6791
|
const configured = (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).find(ch => (ch.id ?? ch.type) === owner.adapterId);
|
|
@@ -8876,11 +6875,7 @@ export class FleetManager {
|
|
|
8876
6875
|
}
|
|
8877
6876
|
getPublicWebLink() {
|
|
8878
6877
|
this.initializeWebSessions();
|
|
8879
|
-
return this.publicWebLink ??= new PublicWebLink(
|
|
8880
|
-
}
|
|
8881
|
-
/** The public link's wiring to this fleet (a test builds one with the tunnel itself stubbed: `ensure`, `provider`). */
|
|
8882
|
-
publicWebLinkDeps() {
|
|
8883
|
-
return {
|
|
6878
|
+
return this.publicWebLink ??= new PublicWebLink({
|
|
8884
6879
|
dataDir: this.dataDir, web: () => this.fleetConfig?.web,
|
|
8885
6880
|
permitted: owner => this.publicOwnerCurrent(owner),
|
|
8886
6881
|
reserve: id => this.getTunnelLane().reserve("dashboard", id),
|
|
@@ -8913,9 +6908,7 @@ export class FleetManager {
|
|
|
8913
6908
|
if (world && world.adapter === owner.binding)
|
|
8914
6909
|
void world.adapter.sendText(owner.chatId, t("dashboard.public_cleanup"), { threadId: owner.threadId }).catch(() => { });
|
|
8915
6910
|
},
|
|
8916
|
-
|
|
8917
|
-
this.outsideLinkClosed(reason); },
|
|
8918
|
-
};
|
|
6911
|
+
});
|
|
8919
6912
|
}
|
|
8920
6913
|
publicLinkAutoOpen = null;
|
|
8921
6914
|
/**
|
|
@@ -9336,7 +7329,6 @@ export class FleetManager {
|
|
|
9336
7329
|
void this.publicWebLink?.close("dashboard revoke");
|
|
9337
7330
|
this.forgetPublicLinkOwner();
|
|
9338
7331
|
this.webLoginCodes.revoke();
|
|
9339
|
-
this.webDeviceLogins?.revokeAll(); // …and every new device still waiting to be approved (#3061)
|
|
9340
7332
|
this.webGuestLoginCodes.revokeAll();
|
|
9341
7333
|
this.guestRevokes++; // and every guest /web still on its way
|
|
9342
7334
|
// #1570: no browser may ask the sign-in page for a code any more.
|
|
@@ -10275,9 +8267,6 @@ export class FleetManager {
|
|
|
10275
8267
|
const fleet = this.loadConfig(configPath, { startup: true });
|
|
10276
8268
|
this.slimFleetConfigAtStartup();
|
|
10277
8269
|
this.logConfigWarningsAtStartup();
|
|
10278
|
-
this.initializeNativeInstances();
|
|
10279
|
-
// M-2255: from here on a change to the Windows runtimes' switches applies live (observeRuntimeSwitches).
|
|
10280
|
-
this.runtimeEffects = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
10281
8270
|
setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
|
|
10282
8271
|
// #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
|
|
10283
8272
|
const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
|
|
@@ -10300,9 +8289,20 @@ export class FleetManager {
|
|
|
10300
8289
|
// #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
|
|
10301
8290
|
const topicMode = daemonTopicMode(fleet);
|
|
10302
8291
|
// Set tmux socket isolation for custom AGEND_HOME
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
8292
|
+
const { getTmuxSocketName: getSocket, isDefaultAgendHome } = await import("./paths.js");
|
|
8293
|
+
TmuxManager.setSocketName(getSocket());
|
|
8294
|
+
// M-2457 §7: after a raw downgrade from 2.4, its own tmux server may still run the agents — start none beside it.
|
|
8295
|
+
// main / the 2.3 line ONLY: on the 2.4 line (release/2.3.5-alpha) AgEnD's own server IS the one on `agend` — a
|
|
8296
|
+
// main → alpha sync must keep alpha's side of this block, never both.
|
|
8297
|
+
{
|
|
8298
|
+
const { newerTmuxServer, realGuardDeps, guardMessage } = await import("./bundled-tmux-guard.js");
|
|
8299
|
+
const hit = await newerTmuxServer(realGuardDeps(getAgendHome(), isDefaultAgendHome(), getSocket()));
|
|
8300
|
+
if (hit) {
|
|
8301
|
+
const message = guardMessage(hit);
|
|
8302
|
+
this.logger.error({ socket: hit.socket, confirmed: hit.confirmed }, message);
|
|
8303
|
+
throw new Error(message);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
10306
8306
|
await TmuxManager.ensureSession(getTmuxSession());
|
|
10307
8307
|
this.tmuxSessionEnsured = true;
|
|
10308
8308
|
// #2072: what this host can run — probed in the background, published for every picker.
|
|
@@ -10310,7 +8310,7 @@ export class FleetManager {
|
|
|
10310
8310
|
// Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
|
|
10311
8311
|
// before instances spawn, so a CLI that comes up on its sign-in screen gets
|
|
10312
8312
|
// an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
|
|
10313
|
-
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).
|
|
8313
|
+
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).map(config => {
|
|
10314
8314
|
const backend = config.backend ?? fleet.defaults?.backend ?? "claude-code";
|
|
10315
8315
|
// #1699 follow-up: each instance's own login — a profile is probed with its own environment.
|
|
10316
8316
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, fleet.defaults, backend) : null };
|
|
@@ -10372,7 +8372,6 @@ export class FleetManager {
|
|
|
10372
8372
|
// #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
|
|
10373
8373
|
// rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
|
|
10374
8374
|
this.restoreWebChat(fleet);
|
|
10375
|
-
this.initializeNativeInstances(true);
|
|
10376
8375
|
this.startSearchIndex(fleet);
|
|
10377
8376
|
// Parse-only source monitoring; it does not apply hand-edited Fleet settings.
|
|
10378
8377
|
this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
|
|
@@ -10555,7 +8554,7 @@ export class FleetManager {
|
|
|
10555
8554
|
}
|
|
10556
8555
|
}
|
|
10557
8556
|
// Phase 1: Start general instances first and wait for them
|
|
10558
|
-
const allEntries = Object.entries(fleet.instances)
|
|
8557
|
+
const allEntries = Object.entries(fleet.instances);
|
|
10559
8558
|
const generals = allEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
10560
8559
|
const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
10561
8560
|
const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
|
|
@@ -10567,9 +8566,6 @@ export class FleetManager {
|
|
|
10567
8566
|
await this.startHealthServer(fleet.health_port ?? 19280);
|
|
10568
8567
|
if (this.shuttingDown)
|
|
10569
8568
|
return;
|
|
10570
|
-
// S-51: from here on the Hub switches apply (an enabled Hub mounts now, beside the fleet; it never blocks startup).
|
|
10571
|
-
this.hubEffectsReady = true;
|
|
10572
|
-
this.observeHubSwitches();
|
|
10573
8569
|
if (generals.length > 0) {
|
|
10574
8570
|
for (const [name, cfg] of generals) {
|
|
10575
8571
|
try {
|
|
@@ -10800,10 +8796,6 @@ export class FleetManager {
|
|
|
10800
8796
|
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
10801
8797
|
this.startPublicLinkAutoOpen();
|
|
10802
8798
|
}
|
|
10803
|
-
// #3008: connections from outside were on when the fleet stopped: the link opens again (its address is new for a
|
|
10804
|
-
// quick link — Settings shows it on this computer).
|
|
10805
|
-
if (this.outsideOn())
|
|
10806
|
-
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
10807
8799
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
10808
8800
|
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
10809
8801
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
@@ -10856,7 +8848,43 @@ export class FleetManager {
|
|
|
10856
8848
|
* Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
|
|
10857
8849
|
* still bounds the vendor calls), recorded like any other.
|
|
10858
8850
|
*/
|
|
8851
|
+
quotaNotices = null;
|
|
8852
|
+
stopQuotaObserver = null;
|
|
8853
|
+
quotaNoticePolicy() {
|
|
8854
|
+
const defaults = this.fleetConfig?.defaults.quota_notice;
|
|
8855
|
+
const wanted = defaults?.target ?? "general";
|
|
8856
|
+
const instances = this.fleetConfig?.instances ?? {};
|
|
8857
|
+
const target = wanted === "general"
|
|
8858
|
+
? Object.entries(instances).find(([name, c]) => c.general_topic === true || name === "general")?.[0]
|
|
8859
|
+
: Object.hasOwn(instances, wanted) ? wanted : undefined;
|
|
8860
|
+
const webOnly = !!this.webOnlyPromptPlace();
|
|
8861
|
+
const config = target ? instances[target] : undefined;
|
|
8862
|
+
const adapterId = target ? this.getInstanceAdapterId(target) : undefined;
|
|
8863
|
+
const groupId = target ? this.getChannelConfig(adapterId)?.group_id : undefined;
|
|
8864
|
+
const ready = !!target && (webOnly || (config?.topic_id != null && !!groupId && !!(this.getAdapterForInstance(target) ?? this.adapter)));
|
|
8865
|
+
return {
|
|
8866
|
+
target: this.shuttingDown ? "" : ready ? target : !target && wanted === "general" && webOnly ? "/notices" : "",
|
|
8867
|
+
destinationBinding: JSON.stringify([webOnly, adapterId, groupId, config?.topic_id]),
|
|
8868
|
+
locale: this.fleetConfig?.defaults.locale ?? getLocale(),
|
|
8869
|
+
subscriptions: this.shuttingDown ? [] : this.activeBackendBindings().flatMap(([name, backend, profile]) => {
|
|
8870
|
+
const provider = usageProviderIdForBackend(backend);
|
|
8871
|
+
if (!provider)
|
|
8872
|
+
return [];
|
|
8873
|
+
const providerId = profile ? `${provider}:${profile}` : provider;
|
|
8874
|
+
return [{ account: `${backend}:host:${profile ?? ""}`, providerId,
|
|
8875
|
+
rule: quotaNoticeRule(defaults, backend, instances[name]?.quota_notice ?? this.classicChannels?.getQuotaNoticeByInstance?.(name)) }];
|
|
8876
|
+
}),
|
|
8877
|
+
};
|
|
8878
|
+
}
|
|
10859
8879
|
startQuotaBackgroundReads() {
|
|
8880
|
+
if (!this.quotaNotices) {
|
|
8881
|
+
this.quotaNotices = new QuotaNotices({
|
|
8882
|
+
file: join(this.dataDir, "quota-notices.json"), policy: () => this.quotaNoticePolicy(),
|
|
8883
|
+
send: (target, text) => target === "/notices" ? this.recordWebNotice("fleet", text) : this.notifyInstanceTopic(target, text),
|
|
8884
|
+
warn: err => this.logger.warn({ err }, "Optional quota reminders held: could not retain dispatch state"),
|
|
8885
|
+
});
|
|
8886
|
+
this.stopQuotaObserver = onUsageSnapshot(payload => { void this.quotaNotices?.observe(payload); });
|
|
8887
|
+
}
|
|
10860
8888
|
if (this.quotaReadTimer)
|
|
10861
8889
|
clearInterval(this.quotaReadTimer);
|
|
10862
8890
|
this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
|
|
@@ -11359,8 +9387,8 @@ export class FleetManager {
|
|
|
11359
9387
|
* that should be running is running
|
|
11360
9388
|
* - `degraded` — reachable, but something the operator should look at (an
|
|
11361
9389
|
* adapter retrying, an instance crashed or stopped)
|
|
11362
|
-
* - `down` — no
|
|
11363
|
-
*
|
|
9390
|
+
* - `down` — the fleet cannot do its job: no adapter is connected, so no
|
|
9391
|
+
* message can arrive or be answered
|
|
11364
9392
|
*
|
|
11365
9393
|
* Deliberately does NOT gate the systemd watchdog — see the comment at the
|
|
11366
9394
|
* WATCHDOG timer for why.
|
|
@@ -11393,20 +9421,10 @@ export class FleetManager {
|
|
|
11393
9421
|
if (effectiveStatus === "connected")
|
|
11394
9422
|
connected++;
|
|
11395
9423
|
}
|
|
11396
|
-
const problems = []
|
|
11397
|
-
const rawHub = this.rawFleetConfig?.hub;
|
|
11398
|
-
const hub = rawHub?.enabled === true ? { server: this.shuttingDown ? "stopped"
|
|
11399
|
-
: this.hubRuntime?.serverHealth?.(rawHub) ?? "unavailable" } : undefined;
|
|
11400
|
-
const hubOnly = !!hub && names.length === 0 && this.adapterState.size === 0;
|
|
11401
|
-
if (hub && hub.server !== "ready")
|
|
11402
|
-
problems.push(`Hub server is ${hub.server}`);
|
|
9424
|
+
const problems = [];
|
|
11403
9425
|
const hostMemory = this.memoryPressure.snapshot();
|
|
11404
|
-
if (hostMemory.level === "critical" || hostMemory.level === "elevated") {
|
|
11405
|
-
|
|
11406
|
-
if (hubOnly)
|
|
11407
|
-
warnings.push(pressure);
|
|
11408
|
-
else if (!this.memoryPressure.advisoryOnly())
|
|
11409
|
-
problems.push(pressure);
|
|
9426
|
+
if (!this.memoryPressure.advisoryOnly() && (hostMemory.level === "critical" || hostMemory.level === "elevated")) {
|
|
9427
|
+
problems.push(`host memory pressure is ${hostMemory.level}`);
|
|
11410
9428
|
}
|
|
11411
9429
|
if (this.adapterState.size > 0 && connected === 0)
|
|
11412
9430
|
problems.push("no channel adapter is connected");
|
|
@@ -11418,8 +9436,9 @@ export class FleetManager {
|
|
|
11418
9436
|
}
|
|
11419
9437
|
if (!this.startupComplete)
|
|
11420
9438
|
problems.push("startup has not completed");
|
|
11421
|
-
//
|
|
11422
|
-
|
|
9439
|
+
// "down" is reserved for "cannot receive or answer a message at all". A fleet
|
|
9440
|
+
// with adapters configured but none connected is exactly that.
|
|
9441
|
+
const status = this.adapterState.size > 0 && connected === 0
|
|
11423
9442
|
? "down"
|
|
11424
9443
|
: problems.length > 0 ? "degraded" : "ok";
|
|
11425
9444
|
return {
|
|
@@ -11431,7 +9450,7 @@ export class FleetManager {
|
|
|
11431
9450
|
memory: readFleetMemory(),
|
|
11432
9451
|
hostMemory: this.memoryTop && (hostMemory.level === "elevated" || hostMemory.level === "critical")
|
|
11433
9452
|
? { ...hostMemory, topProcesses: this.memoryTop.processes, topProcessesAt: this.memoryTop.at } : hostMemory,
|
|
11434
|
-
problems,
|
|
9453
|
+
problems,
|
|
11435
9454
|
};
|
|
11436
9455
|
}
|
|
11437
9456
|
/** Start the primary adapter (backward-compatible, sets this.adapter) */
|
|
@@ -11758,14 +9777,10 @@ export class FleetManager {
|
|
|
11758
9777
|
}
|
|
11759
9778
|
}
|
|
11760
9779
|
}
|
|
11761
|
-
const daemon = this.daemons.get(name);
|
|
11762
|
-
const native = daemon?.usesNativePane?.();
|
|
11763
9780
|
const sockPath = join(this.getInstanceDir(name), "channel.sock");
|
|
11764
|
-
if (!
|
|
9781
|
+
if (!existsSync(sockPath))
|
|
11765
9782
|
return;
|
|
11766
|
-
const ipc =
|
|
11767
|
-
? new NativeDaemonClient(daemon, () => this.daemons.get(name) === daemon && (!this.instanceIpcClients.has(name) || this.instanceIpcClients.get(name) === ipc))
|
|
11768
|
-
: new IpcClient(sockPath);
|
|
9783
|
+
const ipc = new IpcClient(sockPath);
|
|
11769
9784
|
try {
|
|
11770
9785
|
await ipc.connect();
|
|
11771
9786
|
this.instanceIpcClients.set(name, ipc);
|
|
@@ -11828,9 +9843,6 @@ export class FleetManager {
|
|
|
11828
9843
|
this.eventLog?.insert(targetInstance, "cross_instance_delivery_failed", {
|
|
11829
9844
|
from: senderSession, correlation_id: correlationId, error,
|
|
11830
9845
|
});
|
|
11831
|
-
// A Hub question the pane never took: the representative holds it uncertain (never resent).
|
|
11832
|
-
if (senderSession === HUB_REPORT_TARGET && !senderInstance)
|
|
11833
|
-
this.hubDeliveryFailed?.(correlationId, targetInstance);
|
|
11834
9846
|
if (senderInstance) {
|
|
11835
9847
|
this.notifyInstanceTopic(senderInstance, t("cross_instance.delivery_failed_notice", targetInstance, displayError, correlationId));
|
|
11836
9848
|
void this.deliverToInstance(senderInstance, {
|
|
@@ -11884,7 +9896,7 @@ export class FleetManager {
|
|
|
11884
9896
|
// connected, so seed the cache instead of waiting for another transition.
|
|
11885
9897
|
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
11886
9898
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
11887
|
-
if (!
|
|
9899
|
+
if (!this.statuslineWatcher.has(name)) {
|
|
11888
9900
|
this.statuslineWatcher.watch(name);
|
|
11889
9901
|
}
|
|
11890
9902
|
// Auto-reconnect on disconnect (unless intentionally stopping)
|
|
@@ -11949,12 +9961,13 @@ export class FleetManager {
|
|
|
11949
9961
|
try {
|
|
11950
9962
|
const { execFile } = await import("./child-process.js");
|
|
11951
9963
|
const { promisify } = await import("node:util");
|
|
11952
|
-
const {
|
|
9964
|
+
const { getTmuxSocketName } = await import("./paths.js");
|
|
11953
9965
|
// Honour socket isolation: without -L this queried the user's default
|
|
11954
9966
|
// tmux server instead of the fleet's, so under a custom AGEND_HOME the
|
|
11955
9967
|
// check was meaningless (it reported every pane dead).
|
|
11956
|
-
const
|
|
11957
|
-
|
|
9968
|
+
const socket = getTmuxSocketName();
|
|
9969
|
+
const args = socket ? ["-L", socket, "list-panes", "-t", windowId] : ["list-panes", "-t", windowId];
|
|
9970
|
+
await promisify(execFile)("tmux", args, { timeout: 5_000 });
|
|
11958
9971
|
}
|
|
11959
9972
|
catch {
|
|
11960
9973
|
// Pane dead — respawn
|
|
@@ -12958,12 +10971,6 @@ export class FleetManager {
|
|
|
12958
10971
|
if (msg.adapterId)
|
|
12959
10972
|
this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
|
|
12960
10973
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
12961
|
-
const locationHold = this.generalLocationDeliveryHold(generalInstance);
|
|
12962
|
-
if (locationHold) {
|
|
12963
|
-
await inboundAdapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
12964
|
-
this.needsYou?.poke();
|
|
12965
|
-
return;
|
|
12966
|
-
}
|
|
12967
10974
|
// React immediately — before any other API calls — through the status
|
|
12968
10975
|
// path, which tracks Telegram slot ownership and Discord status adds.
|
|
12969
10976
|
if (msg.chatId && msg.messageId) {
|
|
@@ -13051,13 +11058,6 @@ export class FleetManager {
|
|
|
13051
11058
|
if (instanceConfig?.general_topic && await this.topicCommands.handleGeneralCommand(msg, instanceName)) {
|
|
13052
11059
|
return;
|
|
13053
11060
|
}
|
|
13054
|
-
const locationHold = this.generalLocationDeliveryHold(instanceName);
|
|
13055
|
-
if (locationHold) {
|
|
13056
|
-
const adapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
13057
|
-
await adapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
13058
|
-
this.needsYou?.poke();
|
|
13059
|
-
return;
|
|
13060
|
-
}
|
|
13061
11061
|
// Bind instance to the adapter that delivered this message
|
|
13062
11062
|
if (msg.adapterId)
|
|
13063
11063
|
this.bindInstanceAdapter(instanceName, msg.adapterId, true);
|
|
@@ -14666,7 +12666,7 @@ export class FleetManager {
|
|
|
14666
12666
|
const cfg = this.fleetConfig?.instances?.[n];
|
|
14667
12667
|
if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
|
|
14668
12668
|
return null;
|
|
14669
|
-
return this.destinationFor(this.
|
|
12669
|
+
return this.destinationFor(this.getInstanceAdapterId(n), cfg.topic_id);
|
|
14670
12670
|
};
|
|
14671
12671
|
const generalDestinations = () => {
|
|
14672
12672
|
const out = new Set();
|
|
@@ -14685,21 +12685,9 @@ export class FleetManager {
|
|
|
14685
12685
|
};
|
|
14686
12686
|
return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
|
|
14687
12687
|
}
|
|
14688
|
-
/**
|
|
14689
|
-
* The bot an agent's configured topic is on (#3105 review, Prism P1): a fleet.yaml agent's `channel_id` (else the
|
|
14690
|
-
* primary connection) whatever its runtime — a per-turn Windows agent has no execution adapter
|
|
14691
|
-
* (getInstanceAdapterId), yet its topic is still on that bot, and must be counted as an owner and deleted through it.
|
|
14692
|
-
* Topic planning and ownership only; execution routing is getInstanceAdapterId's and unchanged.
|
|
14693
|
-
*/
|
|
14694
|
-
topicAdapterIdOf(name) {
|
|
14695
|
-
const cfg = this.fleetConfig?.instances?.[name];
|
|
14696
|
-
if (cfg)
|
|
14697
|
-
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
14698
|
-
return this.getInstanceAdapterId(name);
|
|
14699
|
-
}
|
|
14700
12688
|
/** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
|
|
14701
12689
|
planInstanceTopicDelete(name) {
|
|
14702
|
-
const adapterId = this.
|
|
12690
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
14703
12691
|
return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
|
|
14704
12692
|
}
|
|
14705
12693
|
getForumTopicDeleter(adapterId) {
|
|
@@ -14979,8 +12967,6 @@ export class FleetManager {
|
|
|
14979
12967
|
this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
|
|
14980
12968
|
if (!this.fleetConfig || !this.configPath)
|
|
14981
12969
|
return;
|
|
14982
|
-
// #3147 r2: a changed or removed Hub representative's consent is invalidated durably BEFORE the change is written.
|
|
14983
|
-
this.observeHubAgentIncarnations(true);
|
|
14984
12970
|
if (!this.savedFleetConfigSnapshot)
|
|
14985
12971
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
14986
12972
|
normalizeNoticeDelivery(this.fleetConfig);
|
|
@@ -15103,12 +13089,9 @@ export class FleetManager {
|
|
|
15103
13089
|
Object.assign(current, resolved);
|
|
15104
13090
|
}
|
|
15105
13091
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
15106
|
-
this.nativeMcpStore?.observePolicies();
|
|
15107
13092
|
this.agentCredentialsCache?.observePolicies();
|
|
15108
13093
|
this.refreshSavedDashboardPermissions();
|
|
15109
13094
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
15110
|
-
this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
|
|
15111
|
-
this.observeHubSwitches(); // S-51: so does a saved Hub switch
|
|
15112
13095
|
}
|
|
15113
13096
|
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
15114
13097
|
refreshSavedDashboardPermissions() {
|
|
@@ -15342,43 +13325,10 @@ export class FleetManager {
|
|
|
15342
13325
|
}
|
|
15343
13326
|
getSettingsConfigPath() { return this.configPath; }
|
|
15344
13327
|
async removeInstance(name, authorization, execution) {
|
|
15345
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name])) {
|
|
15346
|
-
execution?.assert();
|
|
15347
|
-
assertExplicitInstanceRemoval(authorization);
|
|
15348
|
-
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
15349
|
-
await this.stopInstance(name);
|
|
15350
|
-
execution?.assert();
|
|
15351
|
-
}
|
|
15352
|
-
this.nativeInstanceRuntime().remove(name, () => !this.shuttingDown && (!execution || execution.current()), execution);
|
|
15353
|
-
return;
|
|
15354
|
-
}
|
|
15355
13328
|
if (execution)
|
|
15356
13329
|
return this.lifecycle.runTransition(name, transition => this.removeInstanceOwned(name, authorization, execution, transition));
|
|
15357
13330
|
return this.removeInstanceOwned(name, authorization);
|
|
15358
13331
|
}
|
|
15359
|
-
/** Shared instance metadata goes with a native identity; its Windows workspace and provider sessions stay intact. */
|
|
15360
|
-
nativeInstanceRemoved(name) {
|
|
15361
|
-
const clean = (what, run) => { try {
|
|
15362
|
-
run();
|
|
15363
|
-
}
|
|
15364
|
-
catch (error) {
|
|
15365
|
-
this.logger.warn({ name, err: error }, `Native instance ${what} cleanup failed`);
|
|
15366
|
-
} };
|
|
15367
|
-
clean("delivery", () => { this.cancelPendingDeliveries(name); });
|
|
15368
|
-
forgetInstanceContext(name);
|
|
15369
|
-
clean("schedules", () => { for (const schedule of this.scheduler?.list(name) ?? [])
|
|
15370
|
-
this.scheduler.delete(schedule.id); });
|
|
15371
|
-
this.lastActivity.delete(name);
|
|
15372
|
-
this.lastInboundUser.delete(name);
|
|
15373
|
-
this.rateLimitWarnedAt.delete(name);
|
|
15374
|
-
this.lastInboundMsg.delete(name);
|
|
15375
|
-
clean("history", () => this.webChatHistory.forget(name));
|
|
15376
|
-
clean("search", () => this.searchIndex?.removeInstance(name));
|
|
15377
|
-
clean("files", () => this.webFiles.forget(name));
|
|
15378
|
-
clean("compact", () => this.compactWatchService?.forget(name));
|
|
15379
|
-
clean("preferences", () => this.webPrefs.forgetInstance(name));
|
|
15380
|
-
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); });
|
|
15381
|
-
}
|
|
15382
13332
|
async removeInstanceOwned(name, authorization, execution, transition) {
|
|
15383
13333
|
execution?.assert();
|
|
15384
13334
|
assertExplicitInstanceRemoval(authorization);
|
|
@@ -15418,7 +13368,6 @@ export class FleetManager {
|
|
|
15418
13368
|
const instanceDirToClean = this.getInstanceDir(name);
|
|
15419
13369
|
// #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
|
|
15420
13370
|
// sent before wait for the outcome before they are shown.
|
|
15421
|
-
this.forgetHubAgentIncarnation(name, true); // #3147: a re-added instance never inherits Hub consent; not durable → refused
|
|
15422
13371
|
const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
|
|
15423
13372
|
this.webChatGenerations.set(name, removedBelow);
|
|
15424
13373
|
let settleRemoval;
|
|
@@ -15505,8 +13454,6 @@ export class FleetManager {
|
|
|
15505
13454
|
execution?.complete();
|
|
15506
13455
|
}
|
|
15507
13456
|
startStatuslineWatcher(name) {
|
|
15508
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
15509
|
-
return;
|
|
15510
13457
|
if (this.lifecycle.isPaused(name))
|
|
15511
13458
|
return;
|
|
15512
13459
|
this.statuslineWatcher.watch(name);
|
|
@@ -16955,10 +14902,6 @@ export class FleetManager {
|
|
|
16955
14902
|
* would have to make this async and change every caller.
|
|
16956
14903
|
*/
|
|
16957
14904
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
16958
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
16959
|
-
this.emitSseEvent("message", { instance: instanceName, role: "status", sender: "AgEnD", text, ts: new Date().toISOString() });
|
|
16960
|
-
return true;
|
|
16961
|
-
}
|
|
16962
14905
|
// #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
|
|
16963
14906
|
if (this.webOnlyPromptPlace())
|
|
16964
14907
|
return this.recordWebNotice("instance", text, instanceName);
|
|
@@ -17403,11 +15346,6 @@ export class FleetManager {
|
|
|
17403
15346
|
needsYouItems() {
|
|
17404
15347
|
return this.needsYou?.webItems() ?? [];
|
|
17405
15348
|
}
|
|
17406
|
-
/** Enabled member composition supplies only a local attention reader, never an approval/execution callback. */
|
|
17407
|
-
attachHubExecutionAttention(read) {
|
|
17408
|
-
this.hubExecutionAttention = read;
|
|
17409
|
-
this.needsYou?.poke();
|
|
17410
|
-
}
|
|
17411
15349
|
/**
|
|
17412
15350
|
* #1389: the org chart's structure for the web — the instances the dashboard lists (getUiStatus's names), General,
|
|
17413
15351
|
* fleet.yaml's teams, each instance's description and thread link. Read on demand; the live state rides the stream.
|
|
@@ -17444,8 +15382,6 @@ export class FleetManager {
|
|
|
17444
15382
|
* (also for a backend that has no profiles, where the option is ignored).
|
|
17445
15383
|
*/
|
|
17446
15384
|
credentialProfileOf(name, backend) {
|
|
17447
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
17448
|
-
return null;
|
|
17449
15385
|
if (!credentialHomeSpec(backend))
|
|
17450
15386
|
return null;
|
|
17451
15387
|
const cfg = this.fleetConfig?.instances?.[name];
|
|
@@ -17468,7 +15404,7 @@ export class FleetManager {
|
|
|
17468
15404
|
}
|
|
17469
15405
|
/** The instances the cache analysis reads: fleet.yaml's, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend). */
|
|
17470
15406
|
cacheInstances() {
|
|
17471
|
-
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).
|
|
15407
|
+
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
17472
15408
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
17473
15409
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
17474
15410
|
...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
|
|
@@ -17652,20 +15588,6 @@ export class FleetManager {
|
|
|
17652
15588
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
17653
15589
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
17654
15590
|
usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
|
|
17655
|
-
nativeDeliveryHold: this.daemons.get(name)?.nativeDeliveryHold?.() ?? null,
|
|
17656
|
-
generalLocation: this.generalLocationAttention(name),
|
|
17657
|
-
paneRecovery: (() => {
|
|
17658
|
-
const config = this.fleetConfig?.instances[name];
|
|
17659
|
-
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
17660
|
-
return null;
|
|
17661
|
-
try {
|
|
17662
|
-
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
17663
|
-
return profile.paneRecovery ? { key: `${profile.paneRecovery.kind}:${profile.paneRecovery.launchId ?? "-"}`, detail: new NativePaneRecoveryError(profile.paneRecovery).message, since: profile.paneRecoveryAt ?? profile.createdAt } : null;
|
|
17664
|
-
}
|
|
17665
|
-
catch {
|
|
17666
|
-
return null;
|
|
17667
|
-
}
|
|
17668
|
-
})(),
|
|
17669
15591
|
});
|
|
17670
15592
|
}
|
|
17671
15593
|
return out;
|
|
@@ -17721,8 +15643,6 @@ export class FleetManager {
|
|
|
17721
15643
|
const target = this.fleetNoticeTarget(world);
|
|
17722
15644
|
return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
|
|
17723
15645
|
},
|
|
17724
|
-
// The name the web page shows for the instance (its View alias, else its own name), never the raw id alone.
|
|
17725
|
-
instanceName: instance => resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[instance], profile: profileIdentities(this.dataDir).get(instance) }).display_name || instance,
|
|
17726
15646
|
instanceTopic: instance => {
|
|
17727
15647
|
const topic = this.fleetConfig?.instances[instance]?.topic_id;
|
|
17728
15648
|
if (topic != null)
|
|
@@ -17745,12 +15665,6 @@ export class FleetManager {
|
|
|
17745
15665
|
acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
|
|
17746
15666
|
tasks: () => this.needsYouTaskItems(),
|
|
17747
15667
|
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
17748
|
-
hubExecution: () => this.hubExecutionAttention?.() ?? [],
|
|
17749
|
-
hubHolds: () => this.hubHolds.map(h => {
|
|
17750
|
-
// A provider hold carries a bounded setup reason; its guidance (S4b's copy) when present, else the code's text.
|
|
17751
|
-
const guidance = h.reason ? t(`hub.provider.${h.reason}`) : "";
|
|
17752
|
-
return { side: h.side, code: h.code, since: h.since, detail: guidance && guidance !== `hub.provider.${h.reason}` ? guidance : t(`hub.hold.${h.code}`) };
|
|
17753
|
-
}),
|
|
17754
15668
|
configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
|
|
17755
15669
|
...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
|
|
17756
15670
|
})),
|
|
@@ -18277,7 +16191,7 @@ export class FleetManager {
|
|
|
18277
16191
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
18278
16192
|
authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
|
|
18279
16193
|
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
|
|
18280
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
16194
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
18281
16195
|
});
|
|
18282
16196
|
}
|
|
18283
16197
|
sessionAuthorizations = null;
|
|
@@ -18337,269 +16251,6 @@ export class FleetManager {
|
|
|
18337
16251
|
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
18338
16252
|
this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
|
|
18339
16253
|
}
|
|
18340
|
-
// ── #3008: connections from outside (a web-only fleet; decision 28c2d3a3 rule 3, the user's simplified form) ──
|
|
18341
|
-
webRemoteAccessState = null;
|
|
18342
|
-
/** The authenticator, the outside switch and the wrong-code limiter (web-remote-access.json), read on first use. */
|
|
18343
|
-
webRemoteAccess() {
|
|
18344
|
-
if (!this.dataDir)
|
|
18345
|
-
return null;
|
|
18346
|
-
this.webRemoteAccessState ??= new WebRemoteAccess(join(this.dataDir, WEB_REMOTE_ACCESS_FILE), this.logger);
|
|
18347
|
-
return this.webRemoteAccessState;
|
|
18348
|
-
}
|
|
18349
|
-
/** "Allow connections from outside" is on (an app set up) and this is a web-only fleet. */
|
|
18350
|
-
outsideOn() {
|
|
18351
|
-
return this.isWebOnlyFleet() && (this.webRemoteAccess()?.outside ?? false);
|
|
18352
|
-
}
|
|
18353
|
-
/** The link connections from outside opened, while it is open and still theirs: its exposure. Else null. */
|
|
18354
|
-
outsideLink() {
|
|
18355
|
-
if (!this.outsideOn())
|
|
18356
|
-
return null;
|
|
18357
|
-
const open = this.publicWebLink?.openSignin();
|
|
18358
|
-
return open && isOutsideLinkOwner(open.owner) ? { exposureId: open.exposureId } : null;
|
|
18359
|
-
}
|
|
18360
|
-
/**
|
|
18361
|
-
* The request's live session, asked again each time `current()` is called (its own cookie authenticated anew, the same
|
|
18362
|
-
* handle): this web-only fleet's owner — this computer's admin, on this computer (`hostOnly`), or also signed in from
|
|
18363
|
-
* outside — and web-verified now. Null when it is not (a guest, a LAN session, a chat fleet's admin, nobody).
|
|
18364
|
-
*/
|
|
18365
|
-
liveOwner(req, opts) {
|
|
18366
|
-
if (!this.isWebOnlyFleet())
|
|
18367
|
-
return null;
|
|
18368
|
-
if (opts.hostOnly && gatewayRequestContext(req))
|
|
18369
|
-
return null;
|
|
18370
|
-
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
18371
|
-
if (auth.kind !== "ok")
|
|
18372
|
-
return null;
|
|
18373
|
-
const handle = auth.session.handle, peer = webPeerOf(req), gateway = !!gatewayRequestContext(req);
|
|
18374
|
-
const owner = () => {
|
|
18375
|
-
if (!this.isWebOnlyFleet())
|
|
18376
|
-
return false;
|
|
18377
|
-
const live = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
18378
|
-
if (live.kind !== "ok" || live.session.handle !== handle)
|
|
18379
|
-
return false;
|
|
18380
|
-
if (opts.hostOnly && (live.session.host !== true || peer !== "loopback"))
|
|
18381
|
-
return false;
|
|
18382
|
-
const now = this.webAuthority({ session: live.session, gateway, peer });
|
|
18383
|
-
const a = now.authority.kind === "web-verified" && now.authority.session?.handle === handle ? now.authority.actor : null;
|
|
18384
|
-
return !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId;
|
|
18385
|
-
};
|
|
18386
|
-
return owner() ? { label: t("settings_confirm.host_operator_label"), handle, current: owner } : null;
|
|
18387
|
-
}
|
|
18388
|
-
/** Every session signed in from outside ends (and the authorizations kept for them). */
|
|
18389
|
-
endOutsideSessions() {
|
|
18390
|
-
const ended = new Set(this.webSessions?.list(undefined, r => r.remote === "totp").map(r => r.handle) ?? []);
|
|
18391
|
-
if (!ended.size)
|
|
18392
|
-
return;
|
|
18393
|
-
this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
|
|
18394
|
-
const result = this.webSessions?.revokeAll(r => r.remote === "totp");
|
|
18395
|
-
if (result && !result.durable)
|
|
18396
|
-
this.logger.warn("Outside sessions ended, but that could not be saved");
|
|
18397
|
-
this.logger.info({ count: ended.size }, "Sessions signed in from outside ended");
|
|
18398
|
-
}
|
|
18399
|
-
/**
|
|
18400
|
-
* #1939 §10 / #3008: a stored receipt's authority as it reads now — the web authority state, and for this computer's
|
|
18401
|
-
* admin from outside the switch and the last revocation of remote authority (Prism P2 2).
|
|
18402
|
-
*/
|
|
18403
|
-
receiptAuthorityNow(stored, state = this.webAuthorityState()) {
|
|
18404
|
-
return authorityNow(stored, { ...state, outside: this.outsideOn(), outsideHandles: this.webRemoteAccess()?.admitted ?? new Set() });
|
|
18405
|
-
}
|
|
18406
|
-
/** #3008 (the sign-in page): this exposure is the outside link, open now. */
|
|
18407
|
-
isOutsideLink(exposureId) {
|
|
18408
|
-
return this.outsideLink()?.exposureId === exposureId;
|
|
18409
|
-
}
|
|
18410
|
-
/** What Settings shows: set up or not, the switch, a wait in force, the link — its address only to this computer. */
|
|
18411
|
-
webOutsideStatus(req) {
|
|
18412
|
-
const store = this.webRemoteAccess(), status = store?.status();
|
|
18413
|
-
const here = !!this.liveOwner(req, { hostOnly: true });
|
|
18414
|
-
const open = this.outsideOn() ? this.publicWebLink?.openSignin() : null;
|
|
18415
|
-
const mine = !!open && isOutsideLinkOwner(open.owner);
|
|
18416
|
-
const linkStatus = this.outsideOn() ? this.getPublicWebStatus() : null;
|
|
18417
|
-
return { webOnly: this.isWebOnlyFleet(), here, enrolled: !!status?.enrolled, enrolledAt: status?.enrolledAt ?? null,
|
|
18418
|
-
outside: this.outsideOn(), pausedForMs: status?.pausedForMs ?? 0,
|
|
18419
|
-
link: linkStatus && linkStatus.state !== "closed" ? { ...linkStatus, ...(here && mine ? { url: open.url } : {}), quick: !publicLinkNamedHostname(this.fleetConfig?.web) } : null };
|
|
18420
|
-
}
|
|
18421
|
-
/** Set up (or set up again): a new secret, shown once to this computer's admin. Null: not them. */
|
|
18422
|
-
webOutsideBeginEnrollment(req) {
|
|
18423
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
18424
|
-
if (!owner || !store)
|
|
18425
|
-
return null;
|
|
18426
|
-
return store.beginEnrollment(owner.handle, hostname() || "fleet");
|
|
18427
|
-
}
|
|
18428
|
-
/** The new app's first code: on "ok" it replaces the old one, whose sessions from outside end. */
|
|
18429
|
-
webOutsideConfirmEnrollment(req, code) {
|
|
18430
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
18431
|
-
if (!owner || !store || !owner.current())
|
|
18432
|
-
return "not_here";
|
|
18433
|
-
const had = store.enrolled;
|
|
18434
|
-
const r = store.confirmEnrollment(owner.handle, code);
|
|
18435
|
-
if (r === "ok") {
|
|
18436
|
-
this.logger.info({ again: had }, "Authenticator app set up for connections from outside");
|
|
18437
|
-
if (had)
|
|
18438
|
-
this.endOutsideSessions();
|
|
18439
|
-
}
|
|
18440
|
-
return r;
|
|
18441
|
-
}
|
|
18442
|
-
/** Forget the app: the switch goes off, the link closes, every session from outside ends. */
|
|
18443
|
-
webOutsideRemove(req) {
|
|
18444
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
18445
|
-
if (!owner || !store || !owner.current())
|
|
18446
|
-
return "not_here";
|
|
18447
|
-
// #3008 review (Prism P2 1): shut at once whatever the disk says; say so when it could not be saved (retried).
|
|
18448
|
-
const saved = store.remove();
|
|
18449
|
-
this.closeOutsideLink("outside removed");
|
|
18450
|
-
this.logger.info({ saved }, "Authenticator app removed; connections from outside off");
|
|
18451
|
-
return saved ? "ok" : "unsaved";
|
|
18452
|
-
}
|
|
18453
|
-
/** The switch. On: needs an app set up; the link opens. Off: it closes and every session from outside ends. */
|
|
18454
|
-
webOutsideSwitch(req, on) {
|
|
18455
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
18456
|
-
if (!owner || !store || !owner.current())
|
|
18457
|
-
return "not_here";
|
|
18458
|
-
if (on && !publicLinkSettings(this.fleetConfig?.web).allowed)
|
|
18459
|
-
return "not_allowed";
|
|
18460
|
-
const r = store.setOutside(on);
|
|
18461
|
-
if (r === "not_enrolled")
|
|
18462
|
-
return r;
|
|
18463
|
-
this.logger.info({ on, saved: r === "ok" }, "Connections from outside switched");
|
|
18464
|
-
// #3008 review (Prism P2 1): Off shuts at once even when it could not be saved (the store retries); On opens only
|
|
18465
|
-
// once saved — a switch the disk does not hold would not survive a restart.
|
|
18466
|
-
if (!on)
|
|
18467
|
-
this.closeOutsideLink("outside off");
|
|
18468
|
-
else if (r === "ok")
|
|
18469
|
-
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
18470
|
-
return r;
|
|
18471
|
-
}
|
|
18472
|
-
outsideAttempt = null;
|
|
18473
|
-
outsideOpening = false;
|
|
18474
|
-
/**
|
|
18475
|
-
* Open the outside link: an attempt after each delay in turn until one opens (#3008 B: twice more at most — the quick
|
|
18476
|
-
* link's address service limits how often it may be asked). Every attempt asks again: the switch still on, the fleet
|
|
18477
|
-
* running. A final failure: one Notice; the switch stays on, so the next start tries again.
|
|
18478
|
-
*/
|
|
18479
|
-
openOutsideLink(delays) {
|
|
18480
|
-
this.outsideAttempt?.cancel();
|
|
18481
|
-
this.outsideAttempt = null;
|
|
18482
|
-
const attempt = (n) => {
|
|
18483
|
-
this.outsideAttempt = null;
|
|
18484
|
-
if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
|
|
18485
|
-
return;
|
|
18486
|
-
this.outsideOpening = true;
|
|
18487
|
-
// alpha CI 4abe58cd: an On right after an Off finds the old link still closing (its tunnel stopping): wait for that
|
|
18488
|
-
// close, then open — never "busy" and a 30 s retry. The switch is asked again after the wait by openUnattended
|
|
18489
|
-
// itself (permitted → publicOwnerCurrent: the fleet running, the switch on): an Off meanwhile opens nothing.
|
|
18490
|
-
const link = this.getPublicWebLink();
|
|
18491
|
-
void link.whenClosed().then(() => link.openUnattended(OUTSIDE_LINK_OWNER))
|
|
18492
|
-
.catch(() => "failed").then(outcome => {
|
|
18493
|
-
this.outsideOpening = false;
|
|
18494
|
-
if (outcome === "opened") {
|
|
18495
|
-
this.logger.info({ attempt: n + 1 }, "Outside link opened");
|
|
18496
|
-
return;
|
|
18497
|
-
}
|
|
18498
|
-
if (outcome === "busy" && this.outsideLink())
|
|
18499
|
-
return; // already open (ours)
|
|
18500
|
-
if (this.shuttingDown || !this.outsideOn())
|
|
18501
|
-
return;
|
|
18502
|
-
if (n + 1 < delays.length) {
|
|
18503
|
-
this.outsideAttempt = outsideTimer(() => attempt(n + 1), delays[n + 1]);
|
|
18504
|
-
return;
|
|
18505
|
-
}
|
|
18506
|
-
this.logger.warn({ attempts: n + 1 }, "Outside link could not be opened; giving up until the next start or switch");
|
|
18507
|
-
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_failed")));
|
|
18508
|
-
});
|
|
18509
|
-
};
|
|
18510
|
-
this.outsideAttempt = outsideTimer(() => attempt(0), delays[0] ?? 0);
|
|
18511
|
-
}
|
|
18512
|
-
closeOutsideLink(reason) {
|
|
18513
|
-
this.outsideAttempt?.cancel();
|
|
18514
|
-
this.outsideAttempt = null;
|
|
18515
|
-
const open = this.publicWebLink?.openSignin();
|
|
18516
|
-
if (this.publicWebLink && (!open || isOutsideLinkOwner(open.owner)))
|
|
18517
|
-
void this.publicWebLink.close(reason);
|
|
18518
|
-
this.endOutsideSessions();
|
|
18519
|
-
}
|
|
18520
|
-
/** PublicWebLink.onClose for the outside link: what closed it decides what happens next (#3008 B). */
|
|
18521
|
-
outsideLinkClosed(reason) {
|
|
18522
|
-
if (this.shuttingDown || OUTSIDE_OWN_CLOSES.has(reason) || !this.outsideOn())
|
|
18523
|
-
return;
|
|
18524
|
-
// #3008 review (Prism P2 3): expiry first — its lifetime counts from the opening request, so it can run out while the
|
|
18525
|
-
// opening is still under way: the switch goes off then too, and that attempt's end (outsideOn false) retries nothing.
|
|
18526
|
-
if (reason === "expired") {
|
|
18527
|
-
// Its lifetime ran out: the switch goes off (only this computer turns it on again — a session from outside can't).
|
|
18528
|
-
this.outsideAttempt?.cancel();
|
|
18529
|
-
this.outsideAttempt = null;
|
|
18530
|
-
if (this.webRemoteAccess()?.setOutside(false) === "unsaved")
|
|
18531
|
-
this.logger.warn("Outside link expired; the switch's Off could not be saved yet (retried)");
|
|
18532
|
-
this.endOutsideSessions();
|
|
18533
|
-
this.logger.info("Outside link expired; connections from outside off");
|
|
18534
|
-
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_expired")));
|
|
18535
|
-
return;
|
|
18536
|
-
}
|
|
18537
|
-
if (this.outsideOpening)
|
|
18538
|
-
return; // that attempt's own failure: its retry
|
|
18539
|
-
// Dropped (the tunnel exited, the gateway failed, the configuration changed): reopened up to twice.
|
|
18540
|
-
this.logger.info({ reason }, "Outside link closed; reopening");
|
|
18541
|
-
this.openOutsideLink(OUTSIDE_LINK_RETRY_MS);
|
|
18542
|
-
}
|
|
18543
|
-
/**
|
|
18544
|
-
* #3008 (auth-api POST /auth/totp): a sign-in through the outside link with the authenticator's code. "ok": the caller
|
|
18545
|
-
* makes the session (SessionRecord.remote) and then calls authorizeOutsideSession. Each wrong code counts; a wait that
|
|
18546
|
-
* starts is a Notice; a code that matches a step just outside the window logs how far this clock seems to be off.
|
|
18547
|
-
*/
|
|
18548
|
-
outsideSignIn(exposureId, code) {
|
|
18549
|
-
const link = this.outsideLink(), store = this.webRemoteAccess();
|
|
18550
|
-
if (!link || !store || !exposureId || link.exposureId !== exposureId)
|
|
18551
|
-
return { kind: "closed" };
|
|
18552
|
-
const v = store.verify(code);
|
|
18553
|
-
if (v.kind === "ok")
|
|
18554
|
-
return { kind: "ok" };
|
|
18555
|
-
if (v.kind === "paused")
|
|
18556
|
-
return v;
|
|
18557
|
-
if (v.kind === "not_enrolled")
|
|
18558
|
-
return { kind: "closed" };
|
|
18559
|
-
if (v.kind === "unavailable")
|
|
18560
|
-
return v;
|
|
18561
|
-
if (v.skewSteps !== null)
|
|
18562
|
-
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");
|
|
18563
|
-
if (v.paused) {
|
|
18564
|
-
this.logger.warn({ waitMs: v.retryAfterMs }, "Sign-in from outside paused after wrong codes");
|
|
18565
|
-
this.recordWebNotice("fleet", t("outside.notice_paused", outsideDuration(v.retryAfterMs), v.wrong));
|
|
18566
|
-
return { kind: "paused", retryAfterMs: v.retryAfterMs };
|
|
18567
|
-
}
|
|
18568
|
-
return { kind: "wrong" };
|
|
18569
|
-
}
|
|
18570
|
-
/**
|
|
18571
|
-
* #3008: a session just made by outsideSignIn's "ok" is this computer's admin for its authorization's life. False: it
|
|
18572
|
-
* could not be admitted (its handle not saved, #3015 review r3) — the caller ends it.
|
|
18573
|
-
*/
|
|
18574
|
-
authorizeOutsideSession(session, peer, label) {
|
|
18575
|
-
const store = this.sessionAuthorizationStore(), link = this.outsideLink();
|
|
18576
|
-
if (!store || session.remote !== "totp" || session.owner || !link || session.exposureId !== link.exposureId)
|
|
18577
|
-
return false;
|
|
18578
|
-
if (!this.webRemoteAccess()?.admit(session.handle)) {
|
|
18579
|
-
this.logger.warn({ handle: session.handle }, "Session from outside not admitted: it could not be saved");
|
|
18580
|
-
return false;
|
|
18581
|
-
}
|
|
18582
|
-
const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
|
|
18583
|
-
const check = evaluateWebAuthority({
|
|
18584
|
-
session, gateway: true, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
18585
|
-
isAdmin: () => false,
|
|
18586
|
-
authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
|
|
18587
|
-
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
18588
|
-
hostOperator: true, outside: link,
|
|
18589
|
-
});
|
|
18590
|
-
if (check.refusal) {
|
|
18591
|
-
this.logger.info({ handle: session.handle, refusal: check.refusal }, "Session from outside not authorized");
|
|
18592
|
-
return false;
|
|
18593
|
-
}
|
|
18594
|
-
{
|
|
18595
|
-
const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
|
|
18596
|
-
store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
|
|
18597
|
-
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
18598
|
-
this.logger.info({ handle: session.handle, durable }, "Session from outside authorized (this computer's admin)");
|
|
18599
|
-
}
|
|
18600
|
-
this.recordWebNotice("fleet", t("outside.notice_signed_in", label));
|
|
18601
|
-
return true;
|
|
18602
|
-
}
|
|
18603
16254
|
authorityStateStore = null;
|
|
18604
16255
|
/**
|
|
18605
16256
|
* #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
|
|
@@ -18620,10 +16271,6 @@ export class FleetManager {
|
|
|
18620
16271
|
return true;
|
|
18621
16272
|
if (session.surface === "local")
|
|
18622
16273
|
return false;
|
|
18623
|
-
// #3008 review (Prism P2 4): signed in from outside with the authenticator's code — its opt-in is the outside
|
|
18624
|
-
// switch, not these options (the authority judges it so): it keeps its authorization.
|
|
18625
|
-
if (session.remote === "totp")
|
|
18626
|
-
return false;
|
|
18627
16274
|
const peer = a.peer ?? "named_link";
|
|
18628
16275
|
return (!!state.namedLinkClosed && peer === "named_link") || (!!state.quickLinkClosed && peer === "quick_link");
|
|
18629
16276
|
};
|
|
@@ -18667,7 +16314,7 @@ export class FleetManager {
|
|
|
18667
16314
|
// Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
|
|
18668
16315
|
...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
|
|
18669
16316
|
const host = this.fleetConfig?.web?.public_link?.hostname;
|
|
18670
|
-
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null
|
|
16317
|
+
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null };
|
|
18671
16318
|
}
|
|
18672
16319
|
authorizationRequests = null;
|
|
18673
16320
|
/**
|
|
@@ -18775,7 +16422,7 @@ export class FleetManager {
|
|
|
18775
16422
|
authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
|
|
18776
16423
|
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
18777
16424
|
// #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
|
|
18778
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
16425
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
18779
16426
|
});
|
|
18780
16427
|
return r.refusal;
|
|
18781
16428
|
}
|
|
@@ -19641,8 +17288,6 @@ export class FleetManager {
|
|
|
19641
17288
|
publication.retirePending = true;
|
|
19642
17289
|
}
|
|
19643
17290
|
async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
|
|
19644
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
19645
|
-
return;
|
|
19646
17291
|
// Post first, retire after (see the tail of this method). Retiring up front
|
|
19647
17292
|
// meant that from the delete until the new message came back — a chat API
|
|
19648
17293
|
// round trip, and every reply goes through here — the instance had NO live
|
|
@@ -20192,14 +17837,6 @@ export class FleetManager {
|
|
|
20192
17837
|
}
|
|
20193
17838
|
/** Interrupt an instance's current generation (cancel button / /cancel). */
|
|
20194
17839
|
cancelInstance(instanceName) {
|
|
20195
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
20196
|
-
this.cancelPendingDeliveries(instanceName);
|
|
20197
|
-
const cancelled = this.nativeInstanceRuntime().cancel(instanceName);
|
|
20198
|
-
this.lastInboundMsg.delete(instanceName);
|
|
20199
|
-
for (const message of this.webChatHistory.cancelPending(instanceName))
|
|
20200
|
-
this.emitSseEvent("delivery", { instance: instanceName, messageId: message.messageId, delivery: message.delivery });
|
|
20201
|
-
return cancelled !== null;
|
|
20202
|
-
}
|
|
20203
17840
|
const daemon = this.daemons.get(instanceName);
|
|
20204
17841
|
if (!daemon)
|
|
20205
17842
|
return false;
|
|
@@ -20305,8 +17942,6 @@ export class FleetManager {
|
|
|
20305
17942
|
}
|
|
20306
17943
|
catch { /* safe metadata only */ }
|
|
20307
17944
|
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
|
|
20308
|
-
if (event.phase === "done")
|
|
20309
|
-
this.forgetSignInIdentities(); // a sign-in may be a different account now
|
|
20310
17945
|
this.needsYou?.poke();
|
|
20311
17946
|
if (!this.browserSignInOwnerCurrent(owner))
|
|
20312
17947
|
return;
|
|
@@ -20345,10 +17980,7 @@ export class FleetManager {
|
|
|
20345
17980
|
cancel: () => this.cancelLoginSession(),
|
|
20346
17981
|
notice: ({ shown, label, handle }) => this.notifyFleetError(t("login.web_dashboard_notice", shown, label, handle.slice(0, 8))),
|
|
20347
17982
|
// No details in the event: each page reads its own view (GET /ui/backend-login), as its session may see it.
|
|
20348
|
-
|
|
20349
|
-
// running one: `changed` only tells the pages to read again.
|
|
20350
|
-
changed: () => { broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); },
|
|
20351
|
-
loginEnded: () => this.forgetSignInIdentities(),
|
|
17983
|
+
changed: () => broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")),
|
|
20352
17984
|
});
|
|
20353
17985
|
return this.webBackendLoginsInstance;
|
|
20354
17986
|
}
|
|
@@ -20362,135 +17994,6 @@ export class FleetManager {
|
|
|
20362
17994
|
* #2233: Settings → Agents' sign-in list — every backend's default login as the availability snapshot has it, then
|
|
20363
17995
|
* each credential profile an agent uses (its own login state, read offline).
|
|
20364
17996
|
*/
|
|
20365
|
-
/** Which account each sign-in is, as the host's own `claude auth status` said (masked; see checkAuthIdentity). Keys are `backend\0profile` in a Map: a name is data, never a property. */
|
|
20366
|
-
signInIdentities = new Map();
|
|
20367
|
-
/** Reads in flight, by the same key: a second request for a sign-in that is already being read joins it instead of starting another process. */
|
|
20368
|
-
signInIdentityReads = new Map();
|
|
20369
|
-
/** Raised each time the accounts are forgotten: a read that began before is never allowed to write what it found. */
|
|
20370
|
-
signInIdentityGeneration = 0;
|
|
20371
|
-
signInIdentitySlots = 0;
|
|
20372
|
-
signInIdentityWaiters = [];
|
|
20373
|
-
signInIdentity(backend, profile) { return this.signInIdentities.get(`${backend}\0${profile ?? ""}`)?.identity ?? undefined; }
|
|
20374
|
-
/** A sign-in finished (or changed): forget what was read and let no read that was already running put its answer back. */
|
|
20375
|
-
forgetSignInIdentities() { this.signInIdentityGeneration++; this.signInIdentities.clear(); this.signInIdentityReads.clear(); }
|
|
20376
|
-
/** At most three `claude auth status` at once across the whole host, however many requests ask. */
|
|
20377
|
-
async withSignInIdentitySlot(task) {
|
|
20378
|
-
while (this.signInIdentitySlots >= 3)
|
|
20379
|
-
await new Promise(resolve => this.signInIdentityWaiters.push(resolve));
|
|
20380
|
-
this.signInIdentitySlots++;
|
|
20381
|
-
try {
|
|
20382
|
-
return await task();
|
|
20383
|
-
}
|
|
20384
|
-
finally {
|
|
20385
|
-
this.signInIdentitySlots--;
|
|
20386
|
-
this.signInIdentityWaiters.shift()?.();
|
|
20387
|
-
}
|
|
20388
|
-
}
|
|
20389
|
-
/** One `claude auth status` for a sign-in (the default one, or a credential profile's folder). */
|
|
20390
|
-
async readSignInIdentity(target) {
|
|
20391
|
-
let env;
|
|
20392
|
-
if (target.profile)
|
|
20393
|
-
env = credentialProfileEnv(this.dataDir, target.backend, target.profile) ?? undefined;
|
|
20394
|
-
return checkAuthIdentity(target.backend, env);
|
|
20395
|
-
}
|
|
20396
|
-
/**
|
|
20397
|
-
* Start reading, in the background, the account of each Claude Code sign-in that is signed in (the default and every profile an agent uses) and
|
|
20398
|
-
* not read in the last five minutes. It never waits and never blocks a request: the list shows what is already known, and the pages are told to
|
|
20399
|
-
* read it again when something new was learned. A sign-in already being read is not read twice, and a read that began before the accounts were
|
|
20400
|
-
* forgotten (a sign-in finished meanwhile) is dropped. No login, no network request of ours, nothing logged.
|
|
20401
|
-
*/
|
|
20402
|
-
refreshSignInIdentities() {
|
|
20403
|
-
const now = Date.now(), fresh = (target) => { const entry = this.signInIdentities.get(`${target.backend}\0${target.profile ?? ""}`); return !!entry && now - entry.at < 5 * 60_000; };
|
|
20404
|
-
const started = [];
|
|
20405
|
-
for (const target of this.backendSignInTargets()) {
|
|
20406
|
-
const key = `${target.backend}\0${target.profile ?? ""}`;
|
|
20407
|
-
if (target.state !== "usable" || !authIdentitySupported(target.backend) || fresh(target) || this.signInIdentityReads.has(key))
|
|
20408
|
-
continue;
|
|
20409
|
-
const generation = this.signInIdentityGeneration;
|
|
20410
|
-
const read = this.withSignInIdentitySlot(() => this.readSignInIdentity(target)).then(identity => {
|
|
20411
|
-
if (generation !== this.signInIdentityGeneration)
|
|
20412
|
-
return false; // forgotten while it ran: its answer may be the old account
|
|
20413
|
-
this.signInIdentities.set(key, { identity: identity ?? null, at: Date.now() });
|
|
20414
|
-
return true;
|
|
20415
|
-
}).catch(() => {
|
|
20416
|
-
// A read that threw is an answer too: remembered as "no account" for the same five minutes, so every GET does not try again.
|
|
20417
|
-
if (generation === this.signInIdentityGeneration)
|
|
20418
|
-
this.signInIdentities.set(key, { identity: null, at: Date.now() });
|
|
20419
|
-
return false;
|
|
20420
|
-
}).finally(() => { if (this.signInIdentityReads.get(key) === read)
|
|
20421
|
-
this.signInIdentityReads.delete(key); });
|
|
20422
|
-
this.signInIdentityReads.set(key, read);
|
|
20423
|
-
started.push(read);
|
|
20424
|
-
}
|
|
20425
|
-
if (started.length)
|
|
20426
|
-
void Promise.all(started).then(results => {
|
|
20427
|
-
if (results.some(Boolean))
|
|
20428
|
-
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); // read the list again: it knows more now
|
|
20429
|
-
});
|
|
20430
|
-
}
|
|
20431
|
-
/**
|
|
20432
|
-
* #3212 PR 1: the Accounts view (accounts-view.ts) — every CLI's default sign-in and named profiles (used or only on
|
|
20433
|
-
* disk), who uses each and its weekly usage. Host rows only; a paired computer's rows come with #3190 Part 3c. Its
|
|
20434
|
-
* usage is the usage panel's cached read (never forced), given at most `usageWaitMs` before the view is sent without it.
|
|
20435
|
-
*/
|
|
20436
|
-
async accountsView(usageWaitMs = 1500) {
|
|
20437
|
-
const fleet = this.fleetConfig;
|
|
20438
|
-
const defaultBackend = fleet?.defaults?.backend ?? "claude-code";
|
|
20439
|
-
this.refreshSignInIdentities();
|
|
20440
|
-
const targets = this.backendSignInTargets();
|
|
20441
|
-
const diskProfiles = new Map();
|
|
20442
|
-
for (const backend of PROFILE_CLIS) {
|
|
20443
|
-
const rows = [];
|
|
20444
|
-
for (const profile of profilesOnDisk(this.dataDir, backend)) {
|
|
20445
|
-
let login = "unknown";
|
|
20446
|
-
try {
|
|
20447
|
-
login = credentialProfileLogin(this.dataDir, backend, profile).state;
|
|
20448
|
-
}
|
|
20449
|
-
catch { /* unknown */ }
|
|
20450
|
-
const signIn = dashboardSignInSupported(backend, profile);
|
|
20451
|
-
const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
|
|
20452
|
-
const identity = this.signInIdentity(backend, profile);
|
|
20453
|
-
rows.push({ profile, state: (login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown"), signIn,
|
|
20454
|
-
...(hostCommand ? { hostCommand } : {}), ...(identity ? { identity } : {}) });
|
|
20455
|
-
}
|
|
20456
|
-
diskProfiles.set(backend, rows);
|
|
20457
|
-
}
|
|
20458
|
-
// Host agents only: an agent on a paired Windows computer runs on that computer's accounts (its own scope, later).
|
|
20459
|
-
const agents = Object.keys(fleet?.instances ?? {}).filter(name => !isWindowsInstance(fleet.instances[name])).sort()
|
|
20460
|
-
.map(name => { const backend = this.backendNameForInstance(name); return { name, backend, profile: this.credentialProfileOf(name, backend) }; });
|
|
20461
|
-
const classic = readClassicBindings(this.dataDir).map(b => {
|
|
20462
|
-
const backend = b.backend ?? defaultBackend;
|
|
20463
|
-
return { key: b.key, backend, profile: profileName(classicProfile(b.backend_options, fleet?.defaults?.backend_options, backend)) };
|
|
20464
|
-
});
|
|
20465
|
-
const fleetDefaults = new Map();
|
|
20466
|
-
for (const backend of PROFILE_CLIS) {
|
|
20467
|
-
const p = profileName(profileOf(fleet?.defaults?.backend_options, backend));
|
|
20468
|
-
if (p)
|
|
20469
|
-
fleetDefaults.set(backend, p);
|
|
20470
|
-
}
|
|
20471
|
-
// Weekly usage, from the usage panel's own (cached, never forced) read.
|
|
20472
|
-
const usage = new Map();
|
|
20473
|
-
const ids = new Map(); // provider id → backend\0profile
|
|
20474
|
-
for (const backend of ACCOUNT_CLIS) {
|
|
20475
|
-
const pid = usageProviderIdForBackend(backend);
|
|
20476
|
-
if (!pid)
|
|
20477
|
-
continue;
|
|
20478
|
-
ids.set(pid, `${backend}\0`);
|
|
20479
|
-
for (const p of new Set([...(diskProfiles.get(backend) ?? []).map(d => d.profile), ...targets.filter(t => t.backend === backend && t.profile).map(t => t.profile)]))
|
|
20480
|
-
ids.set(`${pid}:${p}`, `${backend}\0${p}`);
|
|
20481
|
-
}
|
|
20482
|
-
try {
|
|
20483
|
-
const payload = await Promise.race([getUsageSnapshot(false, undefined, { allowStale: true }), new Promise(r => { const tm = setTimeout(() => r(null), usageWaitMs); tm.unref?.(); })]);
|
|
20484
|
-
for (const provider of payload?.providers ?? []) {
|
|
20485
|
-
const at = ids.get(provider.id);
|
|
20486
|
-
const w = at ? weeklyUsage(provider) : undefined;
|
|
20487
|
-
if (at && w)
|
|
20488
|
-
usage.set(at, w);
|
|
20489
|
-
}
|
|
20490
|
-
}
|
|
20491
|
-
catch { /* the view without usage */ }
|
|
20492
|
-
return buildAccountsView({ targets, diskProfiles, agents, classic, fleetDefaults, usage });
|
|
20493
|
-
}
|
|
20494
17997
|
backendSignInTargets() {
|
|
20495
17998
|
const out = [];
|
|
20496
17999
|
for (const row of this.backendAvailabilityRows()) {
|
|
@@ -20502,7 +18005,6 @@ export class FleetManager {
|
|
|
20502
18005
|
...(flow?.remoteLogin === "unsupported" ? { hostCommand: flow.command } : {}),
|
|
20503
18006
|
...(row.state === "not_installed" && install ? { install } : {}),
|
|
20504
18007
|
...(row.state === "too_old" && install ? { update: install } : {}),
|
|
20505
|
-
...(this.signInIdentity(row.backend) ? { identity: this.signInIdentity(row.backend) } : {}),
|
|
20506
18008
|
});
|
|
20507
18009
|
}
|
|
20508
18010
|
const seen = new Set();
|
|
@@ -20520,7 +18022,7 @@ export class FleetManager {
|
|
|
20520
18022
|
const signIn = dashboardSignInSupported(backend, profile);
|
|
20521
18023
|
const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
|
|
20522
18024
|
out.push({ backend, profile, state: login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown", signIn,
|
|
20523
|
-
...(hostCommand ? { hostCommand } : {})
|
|
18025
|
+
...(hostCommand ? { hostCommand } : {}) });
|
|
20524
18026
|
}
|
|
20525
18027
|
return out;
|
|
20526
18028
|
}
|
|
@@ -21345,8 +18847,6 @@ export class FleetManager {
|
|
|
21345
18847
|
this.needsYou?.poke();
|
|
21346
18848
|
const onError = (err) => this.logger.debug({ err }, "SSE client write failed; evicting");
|
|
21347
18849
|
if (event === "message" && data && typeof data === "object") {
|
|
21348
|
-
// #2861: an Office share's dialogue box hears that someone talked to an instance (never what was said).
|
|
21349
|
-
this.officeTalk.noteMessage(data);
|
|
21350
18850
|
// A chat message: record it, and send it WITH its id so a reconnecting stream can ask for what it missed.
|
|
21351
18851
|
const m = data;
|
|
21352
18852
|
const recorded = this.webChatHistory.record({
|
|
@@ -21560,7 +19060,36 @@ export class FleetManager {
|
|
|
21560
19060
|
"antigravity": ".agents/AGENTS.md",
|
|
21561
19061
|
"mock": "CLAUDE.md",
|
|
21562
19062
|
};
|
|
21563
|
-
static GENERAL_INSTRUCTIONS =
|
|
19063
|
+
static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
|
|
19064
|
+
|
|
19065
|
+
You are the fleet coordinator — the central entry point for this AgEnD fleet.
|
|
19066
|
+
You route tasks, manage instances, enforce policies, and synthesize results.
|
|
19067
|
+
Do NOT modify project files directly — delegate file changes to the project's instance.
|
|
19068
|
+
You CAN write code snippets, explain code, and answer technical questions directly.
|
|
19069
|
+
|
|
19070
|
+
## Task Routing
|
|
19071
|
+
|
|
19072
|
+
- **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
|
|
19073
|
+
- **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
|
|
19074
|
+
- **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
|
|
19075
|
+
|
|
19076
|
+
Instance discovery: start with list_instances(); follow the guidance in its response. Prefer reuse; never duplicate a running instance.
|
|
19077
|
+
|
|
19078
|
+
## Reply Contract
|
|
19079
|
+
|
|
19080
|
+
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.
|
|
19081
|
+
|
|
19082
|
+
## After Restart
|
|
19083
|
+
|
|
19084
|
+
BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
|
|
19085
|
+
|
|
19086
|
+
## Playbooks (on-demand skills)
|
|
19087
|
+
|
|
19088
|
+
Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
|
|
19089
|
+
- **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
|
|
19090
|
+
- **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
|
|
19091
|
+
Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
|
|
19092
|
+
`;
|
|
21564
19093
|
/** Ensure the general instance has its project instructions file + knowledge */
|
|
21565
19094
|
ensureGeneralInstructions(workDir, backendName, instanceName) {
|
|
21566
19095
|
measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
|
|
@@ -22582,8 +20111,6 @@ export class FleetManager {
|
|
|
22582
20111
|
: model,
|
|
22583
20112
|
});
|
|
22584
20113
|
const fleetInstance = this.fleetConfig?.instances[instanceName];
|
|
22585
|
-
if (isWindowsInstance(fleetInstance))
|
|
22586
|
-
return fleetInstance.model?.trim() ? done(fleetInstance.model.trim(), "instance") : done("default", "unresolved", "this CLI does not report a default");
|
|
22587
20114
|
if (fleetInstance) {
|
|
22588
20115
|
if (fleetInstance.model?.trim())
|
|
22589
20116
|
return done(fleetInstance.model.trim(), "instance");
|
|
@@ -22665,7 +20192,7 @@ export class FleetManager {
|
|
|
22665
20192
|
if (this.fleetConfig?.defaults?.backend)
|
|
22666
20193
|
backends.add(this.fleetConfig.defaults.backend);
|
|
22667
20194
|
for (const inst of Object.values(this.fleetConfig?.instances ?? {}))
|
|
22668
|
-
if (
|
|
20195
|
+
if (inst.backend)
|
|
22669
20196
|
backends.add(inst.backend);
|
|
22670
20197
|
for (const ch of this.classicChannels?.getAll() ?? [])
|
|
22671
20198
|
if (ch.backend)
|
|
@@ -22680,8 +20207,6 @@ export class FleetManager {
|
|
|
22680
20207
|
// here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
|
|
22681
20208
|
const profiles = new Set();
|
|
22682
20209
|
for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
|
|
22683
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
22684
|
-
continue;
|
|
22685
20210
|
if (this.backendNameForInstance(name) !== "claude-code")
|
|
22686
20211
|
continue;
|
|
22687
20212
|
const profile = this.catalogProfile("claude-code", name);
|
|
@@ -22702,8 +20227,6 @@ export class FleetManager {
|
|
|
22702
20227
|
* instead of presenting an old list as a fresh one.
|
|
22703
20228
|
*/
|
|
22704
20229
|
async getModelOptionsWithSource(instanceName, opts = {}) {
|
|
22705
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22706
|
-
return { models: (await this.listModelCatalog({ instanceName })).models, source: "cache" };
|
|
22707
20230
|
const backendName = this.backendNameForInstance(instanceName);
|
|
22708
20231
|
const profile = this.catalogProfile(backendName, instanceName);
|
|
22709
20232
|
const cached = this.readCliEnv(backendName, profile);
|
|
@@ -22759,21 +20282,6 @@ export class FleetManager {
|
|
|
22759
20282
|
*/
|
|
22760
20283
|
async listModelCatalog(opts = {}) {
|
|
22761
20284
|
const { instanceName } = opts;
|
|
22762
|
-
if (instanceName && isWindowsInstance(this.fleetConfig?.instances[instanceName])) {
|
|
22763
|
-
const config = this.fleetConfig.instances[instanceName], backend = config.backend ?? "codex";
|
|
22764
|
-
// Detection is admin-owned. A read never launches a provider or falls back to this host's account.
|
|
22765
|
-
let inventory = null;
|
|
22766
|
-
try {
|
|
22767
|
-
inventory = await this.runtimeWindowsExecutor().probe({ force: false, machineId: config.machine_id });
|
|
22768
|
-
}
|
|
22769
|
-
catch { /* offline remains unavailable */ }
|
|
22770
|
-
const row = inventory?.clis.find(value => value.id === backend);
|
|
22771
|
-
return { backend, scope: "instance", instance: instanceName, current_model: config.model || null,
|
|
22772
|
-
models: (row?.models ?? []).map(value => ({ id: value.id, label: value.name ?? value.id, ...(value.description ? { description: value.description } : {}) })),
|
|
22773
|
-
source: row?.models?.length ? "cache" : "fallback",
|
|
22774
|
-
...(inventory?.probedAt ? { probed_at: new Date(inventory.probedAt).toISOString() } : {}),
|
|
22775
|
-
note: "Windows runtime's last detected inventory; no coordinator account or live probe is used." };
|
|
22776
|
-
}
|
|
22777
20285
|
if (instanceName) {
|
|
22778
20286
|
const backend = this.backendNameForInstance(instanceName);
|
|
22779
20287
|
const resolved = this.resolveInstanceModel(instanceName);
|
|
@@ -22809,8 +20317,6 @@ export class FleetManager {
|
|
|
22809
20317
|
}
|
|
22810
20318
|
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
22811
20319
|
async instanceScopedModels(instanceName, backend) {
|
|
22812
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22813
|
-
return [];
|
|
22814
20320
|
try {
|
|
22815
20321
|
const inst = this.fleetConfig?.instances?.[instanceName];
|
|
22816
20322
|
const instanceDir = this.getInstanceDir(instanceName);
|
|
@@ -23448,8 +20954,6 @@ export class FleetManager {
|
|
|
23448
20954
|
}
|
|
23449
20955
|
/** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
|
|
23450
20956
|
instanceEffortMetadata(instanceName) {
|
|
23451
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
23452
|
-
return { strategy: "unsupported", levels: [] };
|
|
23453
20957
|
const backend = this.backendNameForInstance(instanceName);
|
|
23454
20958
|
let model = null;
|
|
23455
20959
|
if (backend === "antigravity") {
|
|
@@ -23460,8 +20964,6 @@ export class FleetManager {
|
|
|
23460
20964
|
}
|
|
23461
20965
|
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
23462
20966
|
resolveInstanceEffort(instanceName) {
|
|
23463
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
23464
|
-
return { effort: null, source: "unset" };
|
|
23465
20967
|
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
23466
20968
|
if (own)
|
|
23467
20969
|
return { effort: own, source: "instance" };
|
|
@@ -23499,11 +21001,9 @@ export class FleetManager {
|
|
|
23499
21001
|
* instance respawns.
|
|
23500
21002
|
*/
|
|
23501
21003
|
async applyEffort(instanceName, requested, opts) {
|
|
23502
|
-
// #1490 row 27
|
|
21004
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
23503
21005
|
if (opts?.current && !opts.current())
|
|
23504
21006
|
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
23505
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
23506
|
-
return t("effort.unsupported", this.backendNameForInstance(instanceName));
|
|
23507
21007
|
const level = requested.trim().toLowerCase();
|
|
23508
21008
|
const backendName = this.backendNameForInstance(instanceName);
|
|
23509
21009
|
let strategy = "unsupported";
|
|
@@ -23551,20 +21051,6 @@ export class FleetManager {
|
|
|
23551
21051
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
23552
21052
|
return t("model.invalid_chars");
|
|
23553
21053
|
}
|
|
23554
|
-
const native = this.fleetConfig?.instances[instanceName];
|
|
23555
|
-
if (isWindowsInstance(native)) {
|
|
23556
|
-
if (!isNativePaneInstance(native))
|
|
23557
|
-
return WINDOWS_CONFIG_REFUSED;
|
|
23558
|
-
const catalog = await this.listModelCatalog({ instanceName });
|
|
23559
|
-
if (!catalog.models.some(row => row.id === model))
|
|
23560
|
-
return t("model.unknown_name", model, catalog.models.map(row => row.id).join(", "));
|
|
23561
|
-
const id = native.native_agent_id, machine = native.machine_id;
|
|
23562
|
-
await this.updateNativePaneConfig(instanceName, { model }, () => {
|
|
23563
|
-
const now = this.fleetConfig?.instances[instanceName];
|
|
23564
|
-
return isNativePaneInstance(now) && now.native_agent_id === id && now.machine_id === machine;
|
|
23565
|
-
});
|
|
23566
|
-
return t("model.restart_success", instanceName, model);
|
|
23567
|
-
}
|
|
23568
21054
|
const backendName = this.backendNameForInstance(instanceName);
|
|
23569
21055
|
let strategy = "restart";
|
|
23570
21056
|
try {
|
|
@@ -23753,8 +21239,6 @@ export class FleetManager {
|
|
|
23753
21239
|
* profile's, when it is named and has one), claude's account catalog included. Read only: never a probe.
|
|
23754
21240
|
*/
|
|
23755
21241
|
modelCatalogFor(backend, instance) {
|
|
23756
|
-
if (instance && isWindowsInstance(this.fleetConfig?.instances[instance]))
|
|
23757
|
-
return [];
|
|
23758
21242
|
if (!KNOWN_BACKENDS.includes(backend) || backend === "mock")
|
|
23759
21243
|
return [];
|
|
23760
21244
|
const own = instance && Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, instance) ? instance
|
|
@@ -23768,40 +21252,6 @@ export class FleetManager {
|
|
|
23768
21252
|
}
|
|
23769
21253
|
return out;
|
|
23770
21254
|
}
|
|
23771
|
-
/** Host-default fallback only: Grok has no modeled profile home. Never construct a backend or start a probe. */
|
|
23772
|
-
async cliModelCatalogFor(backend, instance) {
|
|
23773
|
-
if (backend !== "grok")
|
|
23774
|
-
return [];
|
|
23775
|
-
const fleet = this.fleetConfig;
|
|
23776
|
-
const configured = instance && Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) ? fleet.instances[instance] : undefined;
|
|
23777
|
-
const classic = this.classicChannels;
|
|
23778
|
-
const room = instance && !configured ? classic?.getAll().find(channel => channel.instanceName === instance) : undefined;
|
|
23779
|
-
if (instance && !configured && !room)
|
|
23780
|
-
return [];
|
|
23781
|
-
const daemon = room ? this.daemons.get(instance) : undefined;
|
|
23782
|
-
const grokHome = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
23783
|
-
const stillHostDefault = () => {
|
|
23784
|
-
if (this.fleetConfig !== fleet || (process.env.GROK_HOME?.trim() || join(homedir(), ".grok")) !== grokHome)
|
|
23785
|
-
return false;
|
|
23786
|
-
if (configured && (isWindowsInstance(configured) || !Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) || fleet.instances[instance] !== configured))
|
|
23787
|
-
return false;
|
|
23788
|
-
if (room && (this.classicChannels !== classic || classic?.getAll().find(channel => channel.instanceName === instance) !== room || this.daemons.get(instance) !== daemon))
|
|
23789
|
-
return false;
|
|
23790
|
-
// The ordinary profile helpers deliberately ignore Grok: it has no credential-home mapping yet.
|
|
23791
|
-
// Check raw declarations, including invalid ones, rather than mistaking them for the host login.
|
|
23792
|
-
const defaults = fleet?.defaults?.backend_options;
|
|
23793
|
-
const profiles = configured
|
|
23794
|
-
? [configured.backend_options?.grok?.credential_profile, defaults?.grok?.credential_profile]
|
|
23795
|
-
: room
|
|
23796
|
-
? [mergeBackendOptions(defaults, room.backendOptions)?.grok?.credential_profile, daemon?.getConfigSnapshot?.().backend_options?.grok?.credential_profile]
|
|
23797
|
-
: [defaults?.grok?.credential_profile];
|
|
23798
|
-
return profiles.every(profile => profile === undefined || profile === null || profile === "");
|
|
23799
|
-
};
|
|
23800
|
-
if (!stillHostDefault())
|
|
23801
|
-
return [];
|
|
23802
|
-
const models = await readGrokModelCache(grokHome);
|
|
23803
|
-
return stillHostDefault() ? models : [];
|
|
23804
|
-
}
|
|
23805
21255
|
/**
|
|
23806
21256
|
* #2072 (#2078 review): the snapshot as THIS instance would see it. Its sign-in on a backend is its own credential
|
|
23807
21257
|
* profile's when it has one there — the host's default login says nothing about it. A profile's login is a file check
|
|
@@ -24356,6 +21806,7 @@ export class FleetManager {
|
|
|
24356
21806
|
...this.fleetConfig?.defaults,
|
|
24357
21807
|
working_directory: workDir,
|
|
24358
21808
|
lightweight: true,
|
|
21809
|
+
quota_notice: classicIdentity?.quotaNotice,
|
|
24359
21810
|
tool_progress: toolProgress ?? "off",
|
|
24360
21811
|
reply_completion_guard: replyCompletionGuard ?? true,
|
|
24361
21812
|
...(backend ? { backend } : {}),
|
|
@@ -24460,19 +21911,6 @@ export class FleetManager {
|
|
|
24460
21911
|
* settles, so a later genuine stop (after a restart) still does the work.
|
|
24461
21912
|
*/
|
|
24462
21913
|
stopAll() {
|
|
24463
|
-
// S-51: no new Hub admission, dispatch or listener work from this tick on; doStopAll awaits the close.
|
|
24464
|
-
this.hubEffectsReady = false;
|
|
24465
|
-
this.hubRuntime?.fence();
|
|
24466
|
-
this.lifecycle.cancelNativeRecovery?.();
|
|
24467
|
-
this.nativeMcpStore?.closeAll();
|
|
24468
|
-
this.runtimeExecutor?.close();
|
|
24469
|
-
this.runtimeExecutor = null;
|
|
24470
|
-
this.fleetWindowsRuntime = null;
|
|
24471
|
-
this.runtimeHub?.close();
|
|
24472
|
-
this.runtimeHub = null;
|
|
24473
|
-
this.localPaneRelay?.stop();
|
|
24474
|
-
this.localPaneRelay = null;
|
|
24475
|
-
closeWindowsBridge(this);
|
|
24476
21914
|
this.stopAllInFlight ??= this.doStopAll().finally(() => {
|
|
24477
21915
|
// #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
|
|
24478
21916
|
if (this.tmuxExecHelperTimer) {
|
|
@@ -24535,6 +21973,9 @@ export class FleetManager {
|
|
|
24535
21973
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
24536
21974
|
// await in the same tick as the signal handler, so no event can slip in.
|
|
24537
21975
|
this.shuttingDown = true;
|
|
21976
|
+
this.stopQuotaObserver?.();
|
|
21977
|
+
this.stopQuotaObserver = null;
|
|
21978
|
+
const quotaNoticesStopped = this.quotaNotices?.stop();
|
|
24538
21979
|
// Stop receive admission before the first await or any instance shutdown; outbound
|
|
24539
21980
|
// adapters remain available below to retire notices and pending prompts.
|
|
24540
21981
|
for (const state of this.adapterState.values()) {
|
|
@@ -24567,7 +22008,6 @@ export class FleetManager {
|
|
|
24567
22008
|
const settingsControlStopped = settingsControl?.close();
|
|
24568
22009
|
this.publicLinkAutoOpen?.stop();
|
|
24569
22010
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
24570
|
-
this.webRemoteAccessState?.close(); // #3008: an unsaved switch gets its last write
|
|
24571
22011
|
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
24572
22012
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
24573
22013
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
@@ -24581,17 +22021,6 @@ export class FleetManager {
|
|
|
24581
22021
|
// #1565: the web chat's last changes reach the disk (bounded: a slow disk does not hold the shutdown).
|
|
24582
22022
|
const webChatStopped = this.webChatStore ? Promise.race([this.webChatStore.flush().catch(() => { }), new Promise(r => setTimeout(r, 3_000).unref?.())]) : Promise.resolve();
|
|
24583
22023
|
const profileStopped = this.runtimeCpuProfiler?.shutdown("fleet shutdown");
|
|
24584
|
-
// S-51: the Hub closes in the same drain, bounded so an unanswering listener does not hold the shutdown.
|
|
24585
|
-
const hubInit = this.hubRuntimeInit, hubNow = this.hubRuntime;
|
|
24586
|
-
this.hubRuntime = null;
|
|
24587
|
-
this.hubRuntimeInit = null;
|
|
24588
|
-
const hubClose = async () => {
|
|
24589
|
-
const runtime = hubNow ?? (hubInit ? await hubInit.catch(() => null) : null);
|
|
24590
|
-
await runtime?.close();
|
|
24591
|
-
};
|
|
24592
|
-
const hubStopped = hubNow || hubInit
|
|
24593
|
-
? Promise.race([hubClose().catch(err => this.logger.warn({ err: err?.message }, "Hub did not close cleanly")), new Promise(r => setTimeout(r, 10_000).unref?.())])
|
|
24594
|
-
: Promise.resolve();
|
|
24595
22024
|
this.ipcStoppingInstances.add("__fleet_stopping__");
|
|
24596
22025
|
// Release held delivery promises before awaiting daemon shutdown, then
|
|
24597
22026
|
// reject spawn work which has not started. Otherwise a storm backoff could
|
|
@@ -24626,10 +22055,11 @@ export class FleetManager {
|
|
|
24626
22055
|
await publicStopped;
|
|
24627
22056
|
await sharesStopped?.catch(() => { });
|
|
24628
22057
|
await profileStopped;
|
|
24629
|
-
await hubStopped;
|
|
24630
22058
|
await needsStopped;
|
|
24631
22059
|
await settingsControlStopped;
|
|
24632
22060
|
await cacheStopped;
|
|
22061
|
+
await quotaNoticesStopped;
|
|
22062
|
+
this.quotaNotices = null;
|
|
24633
22063
|
await webChatStopped;
|
|
24634
22064
|
await this.cpuProfileControl?.close();
|
|
24635
22065
|
this.cpuProfileControl = null;
|
|
@@ -24965,8 +22395,6 @@ export class FleetManager {
|
|
|
24965
22395
|
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
24966
22396
|
this.loadedFleetSource = previousSource;
|
|
24967
22397
|
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
24968
|
-
this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
|
|
24969
|
-
this.observeHubSwitches(); // S-51: and the Hub's
|
|
24970
22398
|
this.logger.error({
|
|
24971
22399
|
oldCount,
|
|
24972
22400
|
newCount,
|
|
@@ -24986,7 +22414,6 @@ export class FleetManager {
|
|
|
24986
22414
|
// mark every row done and tell the user "changes applied".
|
|
24987
22415
|
return { rejected: why };
|
|
24988
22416
|
}
|
|
24989
|
-
this.nativeMcpStore?.observePolicies();
|
|
24990
22417
|
this.agentCredentialsCache?.observePolicies();
|
|
24991
22418
|
if (this.loadedFleetSource)
|
|
24992
22419
|
this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
|
|
@@ -26607,7 +24034,7 @@ export class FleetManager {
|
|
|
26607
24034
|
const topicMode = daemonTopicMode(fleet);
|
|
26608
24035
|
const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
|
|
26609
24036
|
// Phase 1: generals first
|
|
26610
|
-
const restartEntries = Object.entries(fleet.instances)
|
|
24037
|
+
const restartEntries = Object.entries(fleet.instances);
|
|
26611
24038
|
const restartGenerals = restartEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
26612
24039
|
const restartOthers = restartEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
26613
24040
|
for (const [name, cfg] of restartGenerals) {
|
|
@@ -27061,14 +24488,6 @@ export class FleetManager {
|
|
|
27061
24488
|
return;
|
|
27062
24489
|
}
|
|
27063
24490
|
const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
27064
|
-
const publicUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
27065
|
-
if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
|
|
27066
|
-
&& this.runtimeCoordinator().handlePublicRequest(req, res, publicUrl))
|
|
27067
|
-
return;
|
|
27068
|
-
// The read-only installer files (/runtime/install.ps1 and the bundle ZIP) grant nothing. They answer 404 while remote runtimes
|
|
27069
|
-
// are off or on a public gateway link, and touch no other path.
|
|
27070
|
-
if (this.runtimeInstaller().handle(req, res, publicUrl))
|
|
27071
|
-
return;
|
|
27072
24491
|
// Branding is public, after Host admission: automatic favicon requests
|
|
27073
24492
|
// must not become noisy web-token 401s in the browser console.
|
|
27074
24493
|
// #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
|
|
@@ -27095,8 +24514,7 @@ export class FleetManager {
|
|
|
27095
24514
|
if (decision.reason === "no-credential" && req.method === "GET"
|
|
27096
24515
|
&& String(req.headers.accept ?? "").includes("text/html")
|
|
27097
24516
|
&& isWebPageNavigation(requestPath)) {
|
|
27098
|
-
|
|
27099
|
-
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId), deviceLogin: !link && this.deviceLoginAvailable(), loginQr: this.loginQrEnabled() });
|
|
24517
|
+
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
|
|
27100
24518
|
return;
|
|
27101
24519
|
}
|
|
27102
24520
|
res.writeHead(decision.status);
|
|
@@ -27268,7 +24686,6 @@ export class FleetManager {
|
|
|
27268
24686
|
res.end(JSON.stringify({
|
|
27269
24687
|
...sysInfo,
|
|
27270
24688
|
version: this.currentVersion,
|
|
27271
|
-
build: gatewayRequestContext(req) ? undefined : this.currentBuild,
|
|
27272
24689
|
instances: enriched,
|
|
27273
24690
|
publicLink: this.getPublicWebStatus(),
|
|
27274
24691
|
}));
|
|
@@ -27454,12 +24871,6 @@ export class FleetManager {
|
|
|
27454
24871
|
if (!this.webToken || !this.webSessions)
|
|
27455
24872
|
this.initializeWebAuthTokens();
|
|
27456
24873
|
const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
|
|
27457
|
-
server.on("upgrade", (req, socket, head) => {
|
|
27458
|
-
if (!this.shuttingDown && remoteRuntimeEnabled() && isHostAllowed(req.headers.host, allowedHostNames(this.fleetConfig))
|
|
27459
|
-
&& this.runtimeCoordinator().handleUpgrade(req, socket, head))
|
|
27460
|
-
return;
|
|
27461
|
-
socket.destroy();
|
|
27462
|
-
});
|
|
27463
24874
|
// #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
|
|
27464
24875
|
// more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
|
|
27465
24876
|
// keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
|
|
@@ -27684,36 +25095,9 @@ export class FleetManager {
|
|
|
27684
25095
|
this.logger.debug({ err }, "bots unreadable (getUiStatus)");
|
|
27685
25096
|
}
|
|
27686
25097
|
const instances = names.map(name => {
|
|
27687
|
-
const nativeConfig = this.fleetConfig?.instances[name];
|
|
27688
|
-
if (isNativeExecInstance(nativeConfig)) {
|
|
27689
|
-
let runtime = null;
|
|
27690
|
-
let agent = null;
|
|
27691
|
-
try {
|
|
27692
|
-
runtime = this.nativeInstanceRuntime();
|
|
27693
|
-
agent = runtime.get(name);
|
|
27694
|
-
}
|
|
27695
|
-
catch { /* A damaged native profile is a crashed native row, never a Linux spawn. */ }
|
|
27696
|
-
const display_name = nativeConfig.display_name || agent?.name || name;
|
|
27697
|
-
const backend = nativeConfig.backend || agent?.cli || "codex";
|
|
27698
|
-
return { name, display_name, description: nativeConfig.description || undefined, tags: nativeConfig.tags || [],
|
|
27699
|
-
teams: instanceTeams(this.fleetConfig?.teams, name), runtime: "windows", native_agent_id: nativeConfig.native_agent_id,
|
|
27700
|
-
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
27701
|
-
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}),
|
|
27702
|
-
native_blocked: !agent || agent.blocked === true,
|
|
27703
|
-
native_busy: runtime?.busy(name) ?? false,
|
|
27704
|
-
backend,
|
|
27705
|
-
model: nativeConfig.model || "default", model_source: nativeConfig.model ? "instance" : "cli-default",
|
|
27706
|
-
status: agent && runtime ? runtime.status(name) : "crashed", state: agent && runtime ? runtime.execution(name) : null, execution_state: agent && runtime ? runtime.execution(name) : null,
|
|
27707
|
-
interaction: null, interaction_summary: null, context_pct: null, cost: null,
|
|
27708
|
-
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined,
|
|
27709
|
-
avatar: avatarFor({ name, displayName: display_name }),
|
|
27710
|
-
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 },
|
|
27711
|
-
...(!agent ? { runtime_error: "Windows profile is missing or invalid; restore its saved conversation before using this instance" }
|
|
27712
|
-
: agent.blocked ? { runtime_error: "Windows process outcome is unknown; verify it stopped before recovery" } : {}) };
|
|
27713
|
-
}
|
|
27714
25098
|
// #1956: read once for this instance; its cost, context and model come from it.
|
|
27715
|
-
const statusline =
|
|
27716
|
-
const cost =
|
|
25099
|
+
const statusline = readStatuslineJson(this.dataDir, name);
|
|
25100
|
+
const cost = statusline?.cost?.total_cost_usd ?? 0;
|
|
27717
25101
|
// Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
|
|
27718
25102
|
const classic = classicOnly.includes(name);
|
|
27719
25103
|
const backend = classic
|
|
@@ -27721,7 +25105,7 @@ export class FleetManager {
|
|
|
27721
25105
|
: (this.fleetConfig?.instances[name]?.backend
|
|
27722
25106
|
?? this.fleetConfig?.defaults?.backend
|
|
27723
25107
|
?? "claude-code");
|
|
27724
|
-
const { context } =
|
|
25108
|
+
const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
|
|
27725
25109
|
// context_pct: null when unavailable, not 0
|
|
27726
25110
|
const context_pct = context ?? null;
|
|
27727
25111
|
// Model: Only Claude Code has live statusline; others use the effective resolver.
|
|
@@ -27752,24 +25136,6 @@ export class FleetManager {
|
|
|
27752
25136
|
}
|
|
27753
25137
|
return {
|
|
27754
25138
|
name,
|
|
27755
|
-
general_topic: nativeConfig?.general_topic === true,
|
|
27756
|
-
...(isNativePaneInstance(nativeConfig) ? { runtime: "windows", native_adapter: "pane", native_agent_id: nativeConfig.native_agent_id,
|
|
27757
|
-
...(() => {
|
|
27758
|
-
try {
|
|
27759
|
-
const profile = this.nativeInstanceRuntime().get(name), bound = this.daemons.get(name);
|
|
27760
|
-
return { native_recovery: profile?.paneRecovery ?? (profile?.paneLaunchId && bound?.usesNativePane?.() && !bound.nativePaneCurrent()
|
|
27761
|
-
? { kind: "exit-unconfirmed", launchId: profile.paneLaunchId } : null) };
|
|
27762
|
-
}
|
|
27763
|
-
catch {
|
|
27764
|
-
return { native_recovery: null };
|
|
27765
|
-
}
|
|
27766
|
-
})(),
|
|
27767
|
-
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
|
|
27768
|
-
commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
|
|
27769
|
-
// cc3ce4fe: the version this machine's inventory reports for the agent's CLI, and whether AgEnD has audited it.
|
|
27770
|
-
...this.nativeCliStatus(nativeConfig.backend, nativeConfig.machine_id, this.daemons.get(name)),
|
|
27771
|
-
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
27772
|
-
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
|
|
27773
25139
|
display_name: identity.display_name || undefined,
|
|
27774
25140
|
avatar: image.avatar,
|
|
27775
25141
|
...(image.accent ? { accent: image.accent } : {}),
|
|
@@ -27792,8 +25158,6 @@ export class FleetManager {
|
|
|
27792
25158
|
role: identity.role || undefined,
|
|
27793
25159
|
tags: identity.tags,
|
|
27794
25160
|
teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
|
|
27795
|
-
// #3008 G2: a web-only fleet's General (the agent its owner ticked) — the sidebar pins it first.
|
|
27796
|
-
...(this.isWebOnlyFleet() && this.fleetConfig?.instances[name]?.general_topic === true ? { general: true } : {}),
|
|
27797
25161
|
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
|
|
27798
25162
|
// #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
|
|
27799
25163
|
last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
|
|
@@ -27823,7 +25187,6 @@ export class FleetManager {
|
|
|
27823
25187
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
27824
25188
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|
|
27825
25189
|
connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
|
|
27826
|
-
windows_cli: localWindowsCliEnabled(),
|
|
27827
25190
|
// #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
|
|
27828
25191
|
prefs_rev: this.webPrefs.rev(),
|
|
27829
25192
|
// #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
|
|
@@ -27833,8 +25196,6 @@ export class FleetManager {
|
|
|
27833
25196
|
host_memory: this.hostMemoryIndicator(),
|
|
27834
25197
|
// #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
|
|
27835
25198
|
...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
|
|
27836
|
-
// alpha.2 (M-2300, #2997 review): agents' requests changed — the owner's page reads GET /ui/agent-proposals again.
|
|
27837
|
-
...(this.isWebOnlyFleet() ? { agent_proposals_rev: this.agentProposalsRev } : {}),
|
|
27838
25199
|
// #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
|
|
27839
25200
|
...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
|
|
27840
25201
|
};
|
|
@@ -28277,12 +25638,4 @@ load();
|
|
|
28277
25638
|
</script>
|
|
28278
25639
|
</body>
|
|
28279
25640
|
</html>`;
|
|
28280
|
-
/** alpha.2 (M-2300): the agent an applied Windows request created (its name, for the page's "Open chat"), or null. */
|
|
28281
|
-
export function agentNameOf(view) {
|
|
28282
|
-
const r = view.state === "applied" ? view.outcome?.result : null;
|
|
28283
|
-
if (!r || typeof r !== "object")
|
|
28284
|
-
return null;
|
|
28285
|
-
const o = r, name = o.name ?? o.instance_name ?? (o.agent && typeof o.agent === "object" ? o.agent.name : undefined);
|
|
28286
|
-
return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ? name : null;
|
|
28287
|
-
}
|
|
28288
25641
|
//# sourceMappingURL=fleet-manager.js.map
|