@songsid/agend 2.1.12 → 2.2.0-alpha.2
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 +4 -2
- package/README.zh-TW.md +4 -2
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +90 -7
- package/dist/backend/kiro.js +500 -124
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +51 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +471 -146
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +95 -4
- package/dist/daemon.js +748 -111
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +28 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +339 -12
- package/dist/fleet-manager.js +2623 -461
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +8 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +164 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +16 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +44 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +152 -0
- package/dist/service-activation.js +407 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +63 -1
- package/dist/service-installer.js +141 -16
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +216 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +161 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +199 -35
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +57 -5
- package/dist/tmux-control.js +379 -61
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +205 -63
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +318 -0
- package/dist/ui/chat-strings.js +89 -0
- package/dist/ui/chat-thread.js +246 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +504 -0
- package/dist/ui/panel-fleet.js +427 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +754 -0
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +270 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +654 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +346 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +820 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +142 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +63 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +52 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +129 -0
- package/dist/update-install.js +262 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +85 -14
- package/dist/web-api.js +557 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +18 -7
- package/scripts/preinstall-guard.cjs +55 -0
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
package/dist/web-api.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
2
|
+
import { readBoundedWebBody } from "./web-body.js";
|
|
3
|
+
import { gatewayRequestContext } from "./web-request-context.js";
|
|
4
|
+
import { permitWebContinuation } from "./web-continuation.js";
|
|
5
|
+
/**
|
|
6
|
+
* Web UI HTTP API handler.
|
|
7
|
+
* All /ui/* routes are handled here, extracted from fleet-manager.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { formatWebChannelEcho } from "./web-channel-echo.js";
|
|
10
|
+
import { t } from "./locale.js";
|
|
11
|
+
import { isSafeInstanceName } from "./web-shell-routes.js";
|
|
12
|
+
import { MAX_COMMAND_ARGS } from "./web-commands.js";
|
|
13
|
+
import { sendPanelHtml } from "./web-host-guard.js";
|
|
14
|
+
import { shellRoute } from "./web-shell-routes.js";
|
|
1
15
|
import { readFileSync } from "node:fs";
|
|
2
16
|
import { join, dirname } from "node:path";
|
|
3
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -5,8 +19,12 @@ import { execFileSync } from "node:child_process";
|
|
|
5
19
|
import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
|
|
6
20
|
import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js";
|
|
7
21
|
import { z } from "zod";
|
|
8
|
-
import { isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
22
|
+
import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
23
|
+
import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
|
|
24
|
+
import { attachmentDelivery, displayName, INLINE_MIME, isFileId, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
|
|
25
|
+
import { getAgendHome } from "./paths.js";
|
|
9
26
|
import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
|
|
27
|
+
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
10
28
|
// ── Strict public-facing schemas ────────────────────────────────────────────
|
|
11
29
|
// web-api endpoints must reject unknown fields so the dashboard cannot inject
|
|
12
30
|
// internal-only flags that would reach handleCreate/scheduler/config writers.
|
|
@@ -51,16 +69,20 @@ const ConfigUpdateSchema = z.object({
|
|
|
51
69
|
}).strict();
|
|
52
70
|
const SendMessageSchema = z.object({
|
|
53
71
|
instance: z.string().min(1).max(128),
|
|
54
|
-
message: z.string().
|
|
55
|
-
|
|
72
|
+
message: z.string().max(MAX_TEXT),
|
|
73
|
+
/** Ids from POST /ui/upload, for this instance, not yet sent. */
|
|
74
|
+
attachments: z.array(z.string().regex(/^[0-9a-f]{32}$/)).max(UPLOAD_LIMITS.maxFiles).optional(),
|
|
75
|
+
}).strict().refine(v => v.message.trim().length > 0 || (v.attachments?.length ?? 0) > 0, { message: "a message or at least one file", path: ["message"] });
|
|
76
|
+
/** An instance name that is safe as one path segment (no separator, no NUL, not a dot name). */
|
|
77
|
+
const safeInstanceSegment = (name) => /^[^\\/\x00]+$/.test(name) && name !== "." && name !== "..";
|
|
56
78
|
/**
|
|
57
79
|
* Push a single SSE frame to every client. If a client throws (closed socket
|
|
58
80
|
* after a network drop, etc.), evict it from the set and continue — without
|
|
59
81
|
* the eviction the dead entry leaks forever, and without the try/catch a
|
|
60
82
|
* single dead client breaks delivery to every client iterated after it.
|
|
61
83
|
*/
|
|
62
|
-
export function broadcastSseEvent(clients, event, data, onError) {
|
|
63
|
-
const payload =
|
|
84
|
+
export function broadcastSseEvent(clients, event, data, onError, id) {
|
|
85
|
+
const payload = sseFrame(event, data, id);
|
|
64
86
|
const dead = [];
|
|
65
87
|
for (const client of clients) {
|
|
66
88
|
try {
|
|
@@ -79,6 +101,10 @@ export function broadcastSseEvent(clients, event, data, onError) {
|
|
|
79
101
|
catch { /* socket already gone */ }
|
|
80
102
|
}
|
|
81
103
|
}
|
|
104
|
+
/** One SSE frame; `id` (a chat message's) lets a reconnecting EventSource say what it last saw. */
|
|
105
|
+
export function sseFrame(event, data, id) {
|
|
106
|
+
return `${id !== undefined ? `id: ${id}\n` : ""}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
107
|
+
}
|
|
82
108
|
function parseOrReject(schema, data, res) {
|
|
83
109
|
const r = schema.safeParse(data);
|
|
84
110
|
if (!r.success) {
|
|
@@ -91,20 +117,11 @@ function parseOrReject(schema, data, res) {
|
|
|
91
117
|
}
|
|
92
118
|
const __filename = fileURLToPath(import.meta.url);
|
|
93
119
|
const __dirname = dirname(__filename);
|
|
120
|
+
/** How often a stream is refreshed, and how often it re-checks that its session still stands. */
|
|
121
|
+
export const SSE_HEARTBEAT_MS = 10_000;
|
|
94
122
|
/** Parse JSON body from request. */
|
|
95
|
-
function parseBody(req) {
|
|
96
|
-
return
|
|
97
|
-
let body = "";
|
|
98
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
99
|
-
req.on("end", () => {
|
|
100
|
-
try {
|
|
101
|
-
resolve(JSON.parse(body));
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
reject(new Error("Invalid JSON"));
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
});
|
|
123
|
+
async function parseBody(req) {
|
|
124
|
+
return JSON.parse((await readBoundedWebBody(req, 512 * 1024)).toString("utf8"));
|
|
108
125
|
}
|
|
109
126
|
/** Send JSON response. */
|
|
110
127
|
function json(res, status, data) {
|
|
@@ -119,11 +136,22 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
119
136
|
const path = url.pathname;
|
|
120
137
|
const method = req.method ?? "GET";
|
|
121
138
|
// Auth check for all /ui routes. Defence in depth behind the fleet-manager
|
|
122
|
-
// gate
|
|
123
|
-
//
|
|
124
|
-
// panel instead of comparing null against a missing credential.
|
|
139
|
+
// gate: the same session cookie or header token, and an unset token closes
|
|
140
|
+
// the panel instead of comparing null against a missing credential.
|
|
125
141
|
if (path.startsWith("/ui")) {
|
|
126
|
-
|
|
142
|
+
const approved = isSettingsReplay(req) && method === "POST"
|
|
143
|
+
&& (path === "/ui/config" || path === "/ui/instances" || /^\/ui\/instances\/[^/]+\/delete$/.test(path))
|
|
144
|
+
? settingsRequestExecution(req) : undefined;
|
|
145
|
+
if (approved) {
|
|
146
|
+
try {
|
|
147
|
+
approved.assert();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
json(res, 409, { error: "settings_execution_stale" });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (!isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: !isPassiveWebRead(method, path) })) {
|
|
127
155
|
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
128
156
|
return true;
|
|
129
157
|
}
|
|
@@ -132,16 +160,16 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
132
160
|
return false;
|
|
133
161
|
}
|
|
134
162
|
// ── Static files ───────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
// #1408: the app shell, for exactly the paths the classifier names (src/web-shell-routes.ts). One page for every
|
|
164
|
+
// route; the client router mounts the panel. A malformed instance name is a 400, an unknown one is the shell (the
|
|
165
|
+
// panel says "not found"), so the response never tells which names exist.
|
|
166
|
+
const shell = shellRoute(method, path);
|
|
167
|
+
if (shell?.kind === "malformed") {
|
|
168
|
+
json(res, 400, { error: "invalid instance name" });
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (shell?.kind === "shell") {
|
|
172
|
+
serveAppShell(req, res, ctx, "full");
|
|
145
173
|
return true;
|
|
146
174
|
}
|
|
147
175
|
// Serve JS modules
|
|
@@ -190,22 +218,83 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
190
218
|
return true;
|
|
191
219
|
}
|
|
192
220
|
// ── SSE ────────────────────────────────────────────────
|
|
221
|
+
// The same data as the stream, over plain requests, for a path that cannot carry SSE (Cloudflare Quick
|
|
222
|
+
// Tunnels do not; a buffering proxy looks like a mute server). `after` is the same `<boot>-<id>` cursor
|
|
223
|
+
// as the stream's Last-Event-ID, so polling and the stream can take turns without a message twice or
|
|
224
|
+
// a gap — and a cursor from before a fleet restart gets the new process's backlog. No cursor yet (the page
|
|
225
|
+
// has not seen a single stream message): everything still retained, like a restart's backlog, so a message
|
|
226
|
+
// that arrived while the stream was silent is not skipped by the cursor this answer hands out (#1251
|
|
227
|
+
// review). The page keeps one entry per boot+id, so what it already loaded from /ui/history is not doubled.
|
|
228
|
+
if (method === "GET" && path === "/ui/poll") {
|
|
229
|
+
const history = ctx.webChatHistory;
|
|
230
|
+
const after = url.searchParams.get("after");
|
|
231
|
+
const fresh = after === null || after === ""; // a cursor that is not ours in shape still gets nothing
|
|
232
|
+
json(res, 200, {
|
|
233
|
+
status: ctx.getUiStatus(),
|
|
234
|
+
messages: history ? (fresh ? history.after(0) : history.replayFor(parseLastEventId(after))) : [],
|
|
235
|
+
// The ticks too, so polling never has to re-read a chat's history — that read would count as the person's
|
|
236
|
+
// activity; this poll does not (isPassiveWebRead).
|
|
237
|
+
deliveries: history ? history.deliveries() : [],
|
|
238
|
+
// And the fleet prompts open on the dashboard (C4): prompt events are stream-only too.
|
|
239
|
+
prompts: ctx.listWebPrompts?.() ?? [],
|
|
240
|
+
// #1386: "Needs you" rides the passive channels only — no endpoint of its own to poll.
|
|
241
|
+
needs: ctx.needsYouItems?.() ?? [],
|
|
242
|
+
cursor: history ? `${history.boot}-${history.lastId}` : null,
|
|
243
|
+
});
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
193
246
|
if (method === "GET" && path === "/ui/events") {
|
|
247
|
+
if (gatewayRequestContext(req)) {
|
|
248
|
+
json(res, 404, { error: "Use polling on the public link" });
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
194
251
|
res.writeHead(200, {
|
|
195
252
|
"Content-Type": "text/event-stream",
|
|
196
253
|
"Cache-Control": "no-cache",
|
|
197
254
|
Connection: "keep-alive",
|
|
198
255
|
});
|
|
199
256
|
res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`);
|
|
257
|
+
// An EventSource that reconnects says what it last saw: send what it missed, before anything new.
|
|
258
|
+
const lastSeen = parseLastEventId(req.headers["last-event-id"]);
|
|
259
|
+
if (lastSeen !== null && ctx.webChatHistory) {
|
|
260
|
+
const history = ctx.webChatHistory;
|
|
261
|
+
for (const m of history.replayFor(lastSeen))
|
|
262
|
+
res.write(sseFrame("message", m, history.cursorOf(m)));
|
|
263
|
+
}
|
|
264
|
+
// Ticks change messages already sent and carry no cursor: every (re)connect gets where each one is now, so a
|
|
265
|
+
// report that fell into a gap is not lost until a reload (#1253 review). No id: it is not a message.
|
|
266
|
+
if (ctx.webChatHistory)
|
|
267
|
+
res.write(`event: deliveries\ndata: ${JSON.stringify(ctx.webChatHistory.deliveries())}\n\n`);
|
|
268
|
+
// The same for the fleet prompts (C4): one posted, answered or expired during a gap is caught up here.
|
|
269
|
+
if (ctx.listWebPrompts)
|
|
270
|
+
res.write(`event: prompts\ndata: ${JSON.stringify(ctx.listWebPrompts())}\n\n`);
|
|
271
|
+
// #1386: and "Needs you" as it is now; changes follow as `needs` events.
|
|
272
|
+
if (ctx.needsYouItems)
|
|
273
|
+
res.write(`event: needs\ndata: ${JSON.stringify({ items: ctx.needsYouItems() })}\n\n`);
|
|
200
274
|
ctx.sseClients.add(res);
|
|
201
275
|
const interval = setInterval(() => {
|
|
276
|
+
// A stream authorized once must not outlive the authorization. Re-checked
|
|
277
|
+
// without counting as activity, or an open tab would keep an idle session
|
|
278
|
+
// alive forever; a revoked, expired or rotated-away session ends here.
|
|
279
|
+
if (!isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false })) {
|
|
280
|
+
// Close the connection too, not just the response: an ended response leaves the socket idle in
|
|
281
|
+
// keep-alive, still held open by whoever's session just ended.
|
|
282
|
+
const socket = req.socket;
|
|
283
|
+
try {
|
|
284
|
+
res.end(() => socket?.destroy());
|
|
285
|
+
}
|
|
286
|
+
catch { /* already closed */ }
|
|
287
|
+
cleanup();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
202
290
|
try {
|
|
203
|
-
|
|
291
|
+
// Every open dashboard tab, every 10 s: the status of every instance, serialized on the fleet's loop (#1235).
|
|
292
|
+
res.write(measureSyncWork("web.sseStatus", () => `event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`));
|
|
204
293
|
}
|
|
205
294
|
catch {
|
|
206
295
|
cleanup();
|
|
207
296
|
}
|
|
208
|
-
},
|
|
297
|
+
}, ctx.sseHeartbeatMs ?? SSE_HEARTBEAT_MS);
|
|
209
298
|
let cleanedUp = false;
|
|
210
299
|
const cleanup = () => {
|
|
211
300
|
if (cleanedUp)
|
|
@@ -214,20 +303,216 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
214
303
|
ctx.sseClients.delete(res);
|
|
215
304
|
clearInterval(interval);
|
|
216
305
|
};
|
|
217
|
-
// `close`
|
|
218
|
-
//
|
|
306
|
+
// The *response's* `close` is the one that means the stream is over: it fires when the connection
|
|
307
|
+
// goes away or the response is finished. The request's `close` means "the request has been read"
|
|
308
|
+
// — on newer Node that can be as soon as it is consumed, which would drop a live stream from
|
|
309
|
+
// sseClients and stop its session re-check while the response is still open. `error` covers
|
|
310
|
+
// network resets that never deliver a clean FIN. Without these, dead clients accumulate in
|
|
219
311
|
// sseClients and the heartbeat interval keeps firing forever.
|
|
220
|
-
|
|
312
|
+
res.on("close", cleanup);
|
|
221
313
|
req.on("error", cleanup);
|
|
222
314
|
res.on("error", cleanup);
|
|
223
315
|
return true;
|
|
224
316
|
}
|
|
317
|
+
// ── Chat history ───────────────────────────────────────
|
|
318
|
+
if (method === "GET" && path === "/ui/history") {
|
|
319
|
+
const instance = url.searchParams.get("instance") ?? "";
|
|
320
|
+
const limitRaw = url.searchParams.get("limit");
|
|
321
|
+
const limit = limitRaw === null ? 200 : Number(limitRaw);
|
|
322
|
+
if (instance.length === 0 || instance.length > 128 || !Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
323
|
+
json(res, 400, { error: "instance (1-128 chars) and limit (1-500) required" });
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
225
329
|
// ── Send message ───────────────────────────────────────
|
|
226
330
|
if (method === "POST" && path === "/ui/send") {
|
|
227
331
|
handleSendMessage(req, res, ctx);
|
|
228
332
|
return true;
|
|
229
333
|
}
|
|
334
|
+
// ── Files (C2): upload one file for an instance; fetch a file by the id the fleet issued for it ──
|
|
335
|
+
if (method === "POST" && path === "/ui/upload") {
|
|
336
|
+
handleUpload(req, res, url, ctx);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
const fileMatch = path.match(/^\/ui\/file\/([^/]+)$/);
|
|
340
|
+
if (method === "GET" && fileMatch) {
|
|
341
|
+
const id = fileMatch[1];
|
|
342
|
+
const got = isFileId(id) ? ctx.webFiles?.read(id) : null;
|
|
343
|
+
if (!got) {
|
|
344
|
+
json(res, 404, { error: "No such file" });
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
const { file, bytes } = got;
|
|
348
|
+
const inline = INLINE_MIME.has(file.mime);
|
|
349
|
+
// Only the four image types are shown in the page; anything else is a download, never rendered.
|
|
350
|
+
res.setHeader("Content-Type", inline ? file.mime : (file.mime.startsWith("text/") ? "text/plain; charset=utf-8" : "application/octet-stream"));
|
|
351
|
+
res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
|
|
352
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
353
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
354
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
|
|
355
|
+
res.writeHead(200);
|
|
356
|
+
res.end(bytes);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
// ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
|
|
360
|
+
if (method === "GET" && path === "/ui/prompts") {
|
|
361
|
+
json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
// The nonce is the capability, so it travels in the body: never in a URL a log or a history could keep.
|
|
365
|
+
if (method === "POST" && path === "/ui/prompt") {
|
|
366
|
+
if (!ctx.clickWebPrompt) {
|
|
367
|
+
json(res, 404, { error: "No prompts here" });
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const click = ctx.clickWebPrompt.bind(ctx);
|
|
371
|
+
(async () => {
|
|
372
|
+
let body;
|
|
373
|
+
try {
|
|
374
|
+
body = await parseBody(req);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
381
|
+
return;
|
|
382
|
+
if (!body || typeof body !== "object") {
|
|
383
|
+
json(res, 400, { error: "instance, nonce and action required" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const { instance, nonce, action } = body;
|
|
387
|
+
if (typeof instance !== "string" || typeof nonce !== "string" || typeof action !== "string") {
|
|
388
|
+
json(res, 400, { error: "instance, nonce and action required" });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const r = await click(instance, nonce, action);
|
|
392
|
+
// 409: answered elsewhere first, expired, or never open — the page drops the buttons on `gone`.
|
|
393
|
+
json(res, r.status, r.status === 200 ? { answered: true } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
|
|
394
|
+
})().catch(err => {
|
|
395
|
+
ctx.logger.error({ err: err.message }, "Web prompt answer failed");
|
|
396
|
+
try {
|
|
397
|
+
json(res, 500, { error: "Prompt answer failed" });
|
|
398
|
+
}
|
|
399
|
+
catch { /* already answered */ }
|
|
400
|
+
});
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
// #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
|
|
404
|
+
// token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
|
|
405
|
+
if (method === "POST" && path === "/ui/needs/ack") {
|
|
406
|
+
if (!ctx.acknowledgeNeedsItem) {
|
|
407
|
+
json(res, 404, { error: "Nothing to acknowledge here" });
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
const acknowledge = ctx.acknowledgeNeedsItem.bind(ctx);
|
|
411
|
+
(async () => {
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
body = await parseBody(req);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
421
|
+
return;
|
|
422
|
+
const id = body && typeof body === "object" ? body.id : undefined;
|
|
423
|
+
if (typeof id !== "string") {
|
|
424
|
+
json(res, 400, { error: "id required" });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
428
|
+
if (verdict.kind !== "allow") {
|
|
429
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const principal = verdict.via === "session" && verdict.session ? `web:${verdict.session.handle}` : "cli";
|
|
433
|
+
const r = acknowledge(id, principal);
|
|
434
|
+
json(res, r.status, r.status === 200 ? { acknowledged: true, message: r.message } : { error: r.message });
|
|
435
|
+
})().catch(err => {
|
|
436
|
+
ctx.logger.error({ err: err.message }, "Needs you acknowledge failed");
|
|
437
|
+
try {
|
|
438
|
+
json(res, 500, { error: "Acknowledge failed" });
|
|
439
|
+
}
|
|
440
|
+
catch { /* already answered */ }
|
|
441
|
+
});
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
// #1269: an instance's chat command (/ctx, /compact, /clear, /model, …) — a session write (CSRF, checked by the
|
|
445
|
+
// gate), run through the platforms' own handlers and command table. The public link is told it is one (/save is
|
|
446
|
+
// refused there).
|
|
447
|
+
if (method === "POST" && path === "/ui/command") {
|
|
448
|
+
if (!ctx.webCommand) {
|
|
449
|
+
json(res, 404, { error: "Not available" });
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
const run = ctx.webCommand.bind(ctx);
|
|
453
|
+
(async () => {
|
|
454
|
+
let body;
|
|
455
|
+
try {
|
|
456
|
+
body = await parseBody(req);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
463
|
+
return;
|
|
464
|
+
const str = (v, max) => (typeof v === "string" && v.length <= max ? v : undefined);
|
|
465
|
+
const instance = str(body?.instance, 128), command = str(body?.command, 32);
|
|
466
|
+
if (!instance || !isSafeInstanceName(instance) || !command) {
|
|
467
|
+
json(res, 400, { error: "instance and command required" });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (body.args !== undefined && str(body.args, MAX_COMMAND_ARGS) === undefined) {
|
|
471
|
+
json(res, 400, { error: "args too long" });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (body.confirm !== undefined && !(typeof body.confirm === "string" && /^[0-9a-f]{32}$/.test(body.confirm))) {
|
|
475
|
+
json(res, 400, { error: "invalid confirmation" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req) });
|
|
479
|
+
json(res, r.status, r.body);
|
|
480
|
+
})().catch(err => {
|
|
481
|
+
ctx.logger.error({ err: err.message }, "Web command failed");
|
|
482
|
+
try {
|
|
483
|
+
json(res, 500, { error: "Command failed" });
|
|
484
|
+
}
|
|
485
|
+
catch { /* already answered */ }
|
|
486
|
+
});
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
230
489
|
// ── Instance operations ────────────────────────────────
|
|
490
|
+
// Stop the agent's current reply — what Telegram's cancel button and /cancel do: Esc into the CLI, and the
|
|
491
|
+
// messages still waiting for it are dropped. Not /ui/stop, which stops the instance's process.
|
|
492
|
+
const cancelMatch = path.match(/^\/ui\/cancel\/([^/]+)$/);
|
|
493
|
+
if (method === "POST" && cancelMatch) {
|
|
494
|
+
let name;
|
|
495
|
+
try {
|
|
496
|
+
name = decodeURIComponent(cancelMatch[1]);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
json(res, 400, { error: "Bad instance name" });
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
// Own keys only: "constructor" is in every object and names no instance.
|
|
503
|
+
const known = (ctx.fleetConfig ? Object.hasOwn(ctx.fleetConfig.instances, name) : false) || ctx.daemons.has(name);
|
|
504
|
+
if (!ctx.cancelInstance || !known) {
|
|
505
|
+
json(res, 404, { error: `Instance not found: ${name}` });
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
if (!ctx.cancelInstance(name)) {
|
|
509
|
+
json(res, 409, { error: `${name} is not running` });
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
ctx.eventLog?.logActivity("cancel", "web-user", "stop the current reply", name);
|
|
513
|
+
json(res, 200, { cancelled: name });
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
231
516
|
const stopMatch = path.match(/^\/ui\/stop\/(.+)$/);
|
|
232
517
|
if (method === "POST" && stopMatch) {
|
|
233
518
|
const name = decodeURIComponent(stopMatch[1]);
|
|
@@ -278,11 +563,17 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
278
563
|
(async () => {
|
|
279
564
|
try {
|
|
280
565
|
const body = await parseBody(req);
|
|
566
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
567
|
+
return;
|
|
281
568
|
if (body.confirm !== `delete ${name}`) {
|
|
282
569
|
json(res, 400, { error: `Confirmation required: { "confirm": "delete ${name}" }` });
|
|
283
570
|
return;
|
|
284
571
|
}
|
|
285
|
-
|
|
572
|
+
const execution = settingsRequestExecution(req);
|
|
573
|
+
if (execution)
|
|
574
|
+
await ctx.removeInstance(name, authorizeExplicitInstanceRemoval("dashboard-confirmed"), execution);
|
|
575
|
+
else
|
|
576
|
+
await ctx.removeInstance(name, authorizeExplicitInstanceRemoval("dashboard-confirmed"));
|
|
286
577
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
287
578
|
json(res, 200, { deleted: name });
|
|
288
579
|
}
|
|
@@ -364,6 +655,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
364
655
|
(async () => {
|
|
365
656
|
try {
|
|
366
657
|
const body = await parseBody(req);
|
|
658
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
659
|
+
return;
|
|
367
660
|
const v = validateArgs(CreateInstanceArgs, body, "create_instance");
|
|
368
661
|
if (!v.ok) {
|
|
369
662
|
json(res, 400, { error: v.error });
|
|
@@ -371,7 +664,12 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
371
664
|
}
|
|
372
665
|
let result = null;
|
|
373
666
|
let error;
|
|
374
|
-
|
|
667
|
+
const respond = (r, e) => { result = r; error = e; };
|
|
668
|
+
const execution = settingsRequestExecution(req);
|
|
669
|
+
if (execution)
|
|
670
|
+
await ctx.lifecycle.handleCreate(v.data, respond, undefined, execution);
|
|
671
|
+
else
|
|
672
|
+
await ctx.lifecycle.handleCreate(v.data, respond);
|
|
375
673
|
if (error) {
|
|
376
674
|
json(res, 400, { error });
|
|
377
675
|
}
|
|
@@ -404,6 +702,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
404
702
|
(async () => {
|
|
405
703
|
try {
|
|
406
704
|
const body = await parseBody(req);
|
|
705
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
706
|
+
return;
|
|
407
707
|
const parsed = parseOrReject(TaskCreateSchema, body, res);
|
|
408
708
|
if (!parsed)
|
|
409
709
|
return;
|
|
@@ -432,6 +732,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
432
732
|
(async () => {
|
|
433
733
|
try {
|
|
434
734
|
const body = await parseBody(req);
|
|
735
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
736
|
+
return;
|
|
435
737
|
const parsed = parseOrReject(TaskUpdateSchema, body, res);
|
|
436
738
|
if (!parsed)
|
|
437
739
|
return;
|
|
@@ -472,6 +774,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
472
774
|
(async () => {
|
|
473
775
|
try {
|
|
474
776
|
const body = await parseBody(req);
|
|
777
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
778
|
+
return;
|
|
475
779
|
const parsed = parseOrReject(ScheduleCreateSchema, body, res);
|
|
476
780
|
if (!parsed)
|
|
477
781
|
return;
|
|
@@ -499,7 +803,23 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
499
803
|
}
|
|
500
804
|
return true;
|
|
501
805
|
}
|
|
806
|
+
// #1468: the prompt-cache expiry analysis. Opening it is a person's navigation, like any Fleet tab (it counts as
|
|
807
|
+
// use); the page never re-reads it on a timer. Answered from the ledgers as they are; a catch-up runs behind.
|
|
808
|
+
if (method === "GET" && path === "/ui/cache") {
|
|
809
|
+
if (!ctx.cacheReport) {
|
|
810
|
+
json(res, 404, { error: "Not available" });
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
ctx.cacheReport(url.searchParams.get("window") ?? "7d").then((report) => json(res, 200, report), (err) => { ctx.logger.error({ err }, "cache analysis failed"); json(res, 500, { error: "Cache analysis failed" }); });
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
502
816
|
// ── Teams ──────────────────────────────────────────────
|
|
817
|
+
// #1389: the org chart. Opening it is a person's navigation (it counts as use, like a Fleet tab); nothing re-reads
|
|
818
|
+
// it on a timer — the live state comes over the stream the page already has.
|
|
819
|
+
if (method === "GET" && path === "/ui/org") {
|
|
820
|
+
json(res, 200, ctx.orgChart?.() ?? { general: [], teams: [], instances: {} });
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
503
823
|
if (method === "GET" && path === "/ui/teams") {
|
|
504
824
|
const teams = ctx.fleetConfig?.teams ?? {};
|
|
505
825
|
json(res, 200, { teams });
|
|
@@ -509,6 +829,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
509
829
|
(async () => {
|
|
510
830
|
try {
|
|
511
831
|
const body = await parseBody(req);
|
|
832
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
833
|
+
return;
|
|
512
834
|
const parsed = parseOrReject(TeamCreateSchema, body, res);
|
|
513
835
|
if (!parsed)
|
|
514
836
|
return;
|
|
@@ -569,6 +891,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
569
891
|
(async () => {
|
|
570
892
|
try {
|
|
571
893
|
const body = await parseBody(req);
|
|
894
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
895
|
+
return;
|
|
572
896
|
const parsed = parseOrReject(ConfigUpdateSchema, body, res);
|
|
573
897
|
if (!parsed)
|
|
574
898
|
return;
|
|
@@ -578,26 +902,28 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
578
902
|
return;
|
|
579
903
|
}
|
|
580
904
|
const ch = config.channel;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (parsed.channel
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
905
|
+
settingsWrite(req, () => {
|
|
906
|
+
// Update channel settings
|
|
907
|
+
if (parsed.channel && ch) {
|
|
908
|
+
if (parsed.channel.group_id != null)
|
|
909
|
+
config.channel.group_id = parsed.channel.group_id;
|
|
910
|
+
if (parsed.channel.access)
|
|
911
|
+
config.channel.access = parsed.channel.access;
|
|
912
|
+
}
|
|
913
|
+
// Update defaults
|
|
914
|
+
if (parsed.defaults) {
|
|
915
|
+
const d = config.defaults;
|
|
916
|
+
if (parsed.defaults.backend)
|
|
917
|
+
d.backend = parsed.defaults.backend;
|
|
918
|
+
if (parsed.defaults.model)
|
|
919
|
+
d.model = parsed.defaults.model;
|
|
920
|
+
}
|
|
921
|
+
// Update project_roots
|
|
922
|
+
if (parsed.project_roots) {
|
|
923
|
+
config.project_roots = parsed.project_roots;
|
|
924
|
+
}
|
|
925
|
+
ctx.saveFleetConfig();
|
|
926
|
+
});
|
|
601
927
|
const needsRestart = parsed.channel?.group_id != null;
|
|
602
928
|
json(res, 200, { saved: true, needs_restart: needsRestart });
|
|
603
929
|
}
|
|
@@ -611,6 +937,14 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
611
937
|
json(res, 404, { error: "not found" });
|
|
612
938
|
return true;
|
|
613
939
|
}
|
|
940
|
+
/** Keep the echo in a single platform message, including its attachment names. */
|
|
941
|
+
function webEchoPreview(message, names) {
|
|
942
|
+
const text = wellFormed(message.slice(0, 500));
|
|
943
|
+
const files = names.length ? `[📎 ${names.map(name => displayName(name, "attachment")).join(", ")}]` : "";
|
|
944
|
+
const preview = [text, files].filter(Boolean).join(" ");
|
|
945
|
+
const truncated = message.length > 500 || preview.length > 1400;
|
|
946
|
+
return wellFormed(preview.slice(0, 1400)) + (truncated ? ` … (${t("web.echo_full_text")})` : "");
|
|
947
|
+
}
|
|
614
948
|
/** Handle POST /ui/send — extracted for readability. */
|
|
615
949
|
function handleSendMessage(req, res, ctx) {
|
|
616
950
|
let body = "";
|
|
@@ -625,6 +959,8 @@ function handleSendMessage(req, res, ctx) {
|
|
|
625
959
|
body += chunk.toString();
|
|
626
960
|
});
|
|
627
961
|
req.on("end", async () => {
|
|
962
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
963
|
+
return;
|
|
628
964
|
try {
|
|
629
965
|
const raw = JSON.parse(body);
|
|
630
966
|
const parsed = SendMessageSchema.safeParse(raw);
|
|
@@ -633,55 +969,199 @@ function handleSendMessage(req, res, ctx) {
|
|
|
633
969
|
json(res, 400, { error: `${issue.path.join(".") || "body"}: ${issue.message}` });
|
|
634
970
|
return;
|
|
635
971
|
}
|
|
636
|
-
const { instance, message } = parsed.data;
|
|
972
|
+
const { instance, message: typed, attachments = [] } = parsed.data;
|
|
637
973
|
const ipc = ctx.instanceIpcClients.get(instance);
|
|
638
974
|
if (!ipc) {
|
|
639
975
|
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
640
976
|
return;
|
|
641
977
|
}
|
|
978
|
+
let files = [];
|
|
979
|
+
if (attachments.length > 0) {
|
|
980
|
+
const taken = ctx.webFiles?.takeForMessage(instance, attachments);
|
|
981
|
+
if (!taken || !taken.ok) {
|
|
982
|
+
json(res, 400, { error: taken && !taken.ok ? taken.error : "files are not available here" });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
files = taken.entries;
|
|
986
|
+
}
|
|
987
|
+
// The same tags and meta a Telegram photo/document produces, so the agent needs nothing new.
|
|
988
|
+
const delivery = attachmentDelivery(typed, files);
|
|
989
|
+
// #1269: `/raw ` is pasted into the CLI with no [user:] envelope (the daemon's own check, on this exact text).
|
|
990
|
+
// A signed-in local session may do that, as a fleet admin on the platform may; the public link — the outward-
|
|
991
|
+
// facing surface — may not.
|
|
992
|
+
if (gatewayRequestContext(req) && delivery.text.startsWith("/raw ")) {
|
|
993
|
+
ctx.webFiles?.release(files);
|
|
994
|
+
json(res, 403, { error: t("web.raw_public_refused") });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const message = typed;
|
|
642
998
|
const ts = new Date().toISOString();
|
|
999
|
+
// The id the agent is given, and the one its delivery reports come back under: the page's ticks.
|
|
1000
|
+
const messageId = newWebMessageId();
|
|
643
1001
|
// Use real Telegram context so daemon's lastChatId/lastThreadId are set,
|
|
644
1002
|
// enabling reply tool even when first message comes from Web UI.
|
|
645
|
-
// Pure Web UI mode (no channel config) leaves these empty
|
|
646
|
-
//
|
|
647
|
-
const
|
|
1003
|
+
// Pure Web UI mode (no channel config) leaves these empty; the agent's reply then comes back to the
|
|
1004
|
+
// web chat alone (FleetManager's web-only reply sink).
|
|
1005
|
+
const syncAdapter = ctx.getAdapterForInstance ? ctx.getAdapterForInstance(instance) : ctx.adapter;
|
|
1006
|
+
const groupId = ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "");
|
|
648
1007
|
const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
|
|
1008
|
+
const canEcho = ctx.fleetConfig?.web?.echo_to_channel !== false
|
|
1009
|
+
&& !ctx.isClassicInstance?.(instance) && syncAdapter && groupId && topicId != null;
|
|
1010
|
+
// Add no runtime credentials or local attachment paths to the display copy.
|
|
1011
|
+
const preview = webEchoPreview(message, files.map(f => f.name));
|
|
1012
|
+
// ClassicBot echo (#1320 part B): same ordering lane, per-entry
|
|
1013
|
+
// opt-in resolved at send time inside sendClassicWebEcho.
|
|
1014
|
+
const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
|
|
1015
|
+
const settleClassicEcho = canClassicEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
1016
|
+
await ctx.sendClassicWebEcho(instance, formatWebChannelEcho("web-user", preview, t("web.echo_full_text")));
|
|
1017
|
+
}) : undefined;
|
|
1018
|
+
const settleEcho = canEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
1019
|
+
// A replacement adapter or edited binding is not the route we reserved.
|
|
1020
|
+
if ((ctx.getAdapterForInstance ? ctx.getAdapterForInstance(instance) : ctx.adapter) !== syncAdapter
|
|
1021
|
+
|| (ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "")) !== groupId
|
|
1022
|
+
|| ctx.fleetConfig?.instances[instance]?.topic_id !== topicId)
|
|
1023
|
+
return;
|
|
1024
|
+
return syncAdapter.sendText(String(groupId), formatWebChannelEcho("web-user", preview, t("web.echo_full_text")), { threadId: String(topicId), format: "text", allowedMentions: { parse: [] } });
|
|
1025
|
+
}) : undefined;
|
|
1026
|
+
let delivered;
|
|
649
1027
|
try {
|
|
650
|
-
await ctx.deliverToInstance(instance, {
|
|
1028
|
+
delivered = await ctx.deliverToInstance(instance, {
|
|
651
1029
|
type: "fleet_inbound",
|
|
652
|
-
content:
|
|
1030
|
+
content: delivery.text,
|
|
653
1031
|
targetSession: instance,
|
|
654
1032
|
meta: {
|
|
655
1033
|
chat_id: groupId ? String(groupId) : "",
|
|
656
|
-
message_id:
|
|
1034
|
+
message_id: messageId,
|
|
657
1035
|
user: "web-user", user_id: "web-user",
|
|
658
1036
|
ts,
|
|
659
1037
|
thread_id: topicId != null ? String(topicId) : "",
|
|
660
1038
|
source: "web",
|
|
1039
|
+
adapter_id: syncAdapter?.id,
|
|
1040
|
+
...delivery.meta,
|
|
661
1041
|
},
|
|
662
1042
|
});
|
|
663
1043
|
}
|
|
664
1044
|
catch (err) {
|
|
1045
|
+
settleEcho?.(false);
|
|
1046
|
+
// A throw must release the Classic ordering lane too: otherwise the
|
|
1047
|
+
// next accepted echo queues behind a reservation that never settles
|
|
1048
|
+
// and is dropped by expiry (#1330 R2).
|
|
1049
|
+
settleClassicEcho?.(false);
|
|
1050
|
+
ctx.webFiles?.release(files); // not delivered: the same ids can be sent again
|
|
665
1051
|
ctx.logger.error({ err, instance }, "Web message delivery failed");
|
|
666
1052
|
json(res, 503, { error: "Instance delivery failed" });
|
|
667
1053
|
return;
|
|
668
1054
|
}
|
|
1055
|
+
if (delivered === false) {
|
|
1056
|
+
settleEcho?.(false);
|
|
1057
|
+
settleClassicEcho?.(false);
|
|
1058
|
+
ctx.webFiles?.release(files);
|
|
1059
|
+
ctx.logger.error({ instance }, "Web message not delivered (the instance went away or restarted)");
|
|
1060
|
+
json(res, 503, { error: "Instance delivery failed" });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
settleEcho?.(true);
|
|
1064
|
+
settleClassicEcho?.(true);
|
|
1065
|
+
ctx.webFiles?.commit(files);
|
|
669
1066
|
ctx.lastInboundUser.set(instance, "web-user");
|
|
670
|
-
ctx.eventLog?.logActivity("message", "web-user", message.slice(0, 200), instance);
|
|
671
|
-
ctx.emitSseEvent("message", { instance, sender: "web-user", text: message, ts });
|
|
672
|
-
|
|
673
|
-
const syncAdapter = ctx.getAdapterForInstance?.(instance) ?? ctx.adapter;
|
|
674
|
-
const syncGroupId = ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "");
|
|
675
|
-
if (syncAdapter && syncGroupId) {
|
|
676
|
-
const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
|
|
677
|
-
const preview = message.length > 500 ? message.slice(0, 500) + " [...]" : message;
|
|
678
|
-
syncAdapter.sendText(syncGroupId, `🌐 web-user: ${preview}`, { threadId: topicId != null ? String(topicId) : undefined }).catch(() => ctx.logger.debug({}, "Web→Channel sync failed"));
|
|
679
|
-
}
|
|
680
|
-
json(res, 200, { sent: true });
|
|
1067
|
+
ctx.eventLog?.logActivity("message", "web-user", (message || `[${files.length} file(s)]`).slice(0, 200), instance);
|
|
1068
|
+
ctx.emitSseEvent("message", { instance, sender: "web-user", role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId });
|
|
1069
|
+
json(res, 200, { sent: true, messageId });
|
|
681
1070
|
}
|
|
682
1071
|
catch {
|
|
683
1072
|
json(res, 400, { error: "Invalid JSON" });
|
|
684
1073
|
}
|
|
685
1074
|
});
|
|
686
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* One file for one instance's chat. The body is the file itself (Content-Type is ignored: the type is
|
|
1078
|
+
* read from the bytes), `X-Agend-Filename` its name for the label only. Stored in the instance's
|
|
1079
|
+
* workspace inbox under a name chosen here; answered with the id the message will name.
|
|
1080
|
+
*/
|
|
1081
|
+
function handleUpload(req, res, url, ctx) {
|
|
1082
|
+
const instance = url.searchParams.get("instance") ?? "";
|
|
1083
|
+
if (!ctx.webFiles) {
|
|
1084
|
+
json(res, 404, { error: "uploads are not available" });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (!instance || instance.length > 128 || !safeInstanceSegment(instance) || !ctx.instanceIpcClients.has(instance)) {
|
|
1088
|
+
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const declared = Number(req.headers["content-length"] ?? NaN);
|
|
1092
|
+
if (Number.isFinite(declared) && declared > UPLOAD_LIMITS.maxFileBytes) {
|
|
1093
|
+
json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
|
|
1094
|
+
req.resume();
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const chunks = [];
|
|
1098
|
+
let size = 0;
|
|
1099
|
+
let refused = false;
|
|
1100
|
+
req.on("data", (c) => {
|
|
1101
|
+
if (refused)
|
|
1102
|
+
return;
|
|
1103
|
+
size += c.length;
|
|
1104
|
+
if (size > UPLOAD_LIMITS.maxFileBytes) {
|
|
1105
|
+
refused = true;
|
|
1106
|
+
json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
|
|
1107
|
+
req.destroy();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
chunks.push(c);
|
|
1111
|
+
});
|
|
1112
|
+
req.on("error", () => { });
|
|
1113
|
+
req.on("end", () => {
|
|
1114
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1115
|
+
return;
|
|
1116
|
+
if (refused)
|
|
1117
|
+
return;
|
|
1118
|
+
const bytes = Buffer.concat(chunks);
|
|
1119
|
+
const name = displayName(typeof req.headers["x-agend-filename"] === "string" ? req.headers["x-agend-filename"] : "", "file");
|
|
1120
|
+
const type = sniffUpload(bytes, name);
|
|
1121
|
+
if (!type) {
|
|
1122
|
+
json(res, 415, { error: "unsupported file type — images (PNG, JPEG, GIF, WebP), PDF and text files only" });
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
// The instance is checked again now the whole body is here: it may have been deleted (and its files
|
|
1126
|
+
// forgotten) or stopped while the upload streamed in; storing now would bring its id back (#1252 review).
|
|
1127
|
+
// Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
|
|
1128
|
+
// in fleet.yaml, or as a ClassicBot room in classicBot.yaml.
|
|
1129
|
+
const registered = !!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true;
|
|
1130
|
+
if (!ctx.instanceIpcClients.has(instance) || !registered) {
|
|
1131
|
+
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const entry = ctx.webFiles.storeUpload({ instance, inboxDir: join(getAgendHome(), "workspaces", instance, "inbox"), bytes, name, type });
|
|
1136
|
+
json(res, 200, publicAttachment(entry));
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
|
|
1140
|
+
json(res, 500, { error: "the file could not be stored" });
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
function shellPreview(req, ctx, mode) {
|
|
1145
|
+
if (mode !== "full" || gatewayRequestContext(req))
|
|
1146
|
+
return null;
|
|
1147
|
+
return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
|
|
1148
|
+
}
|
|
1149
|
+
function shellBodyTag(req, ctx, mode) {
|
|
1150
|
+
const p = shellPreview(req, ctx, mode);
|
|
1151
|
+
const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
1152
|
+
return `<body data-mode="${mode}"${mode === "full" && gatewayRequestContext(req) ? ' data-web-transport="poll"' : ""} data-dashboard-origin="${attr(p?.dashboardOrigin)}" data-preview-origin="${attr(p?.previewOrigin)}"`
|
|
1153
|
+
+ ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}">`;
|
|
1154
|
+
}
|
|
1155
|
+
/** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
|
|
1156
|
+
* when this load chose one. */
|
|
1157
|
+
export function serveAppShell(req, res, ctx, mode) {
|
|
1158
|
+
try {
|
|
1159
|
+
const html = readFileSync(join(__dirname, "ui", "app.html"), "utf-8");
|
|
1160
|
+
const p = shellPreview(req, ctx, mode);
|
|
1161
|
+
sendPanelHtml(res, html.replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, p?.previewOrigin ? { frameSrc: `${p.previewOrigin}/frame` } : {});
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
json(res, 500, { error: "app.html not found" });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
687
1167
|
//# sourceMappingURL=web-api.js.map
|