@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/settings-api.js
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
|
+
import { normalizeSettingsInstancePatch, removesInstanceOverride } from "./settings-instance-patch.js";
|
|
2
|
+
import { settingsRequestExecution, settingsWrite } from "./settings-request-capability.js";
|
|
3
|
+
import { noteSettingsWrite, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
4
|
+
import { readBoundedWebBody } from "./web-body.js";
|
|
5
|
+
import { validPublicLinkPatch } from "./public-web-link.js";
|
|
6
|
+
import { permitWebContinuation } from "./web-continuation.js";
|
|
7
|
+
/**
|
|
8
|
+
* Settings Web API (`/settings`) — CRUD over fleet.yaml + classicBot.yaml.
|
|
9
|
+
*
|
|
10
|
+
* GET /settings → static page
|
|
11
|
+
* GET /api/settings/fleet → fleet config as JSON
|
|
12
|
+
* GET /api/settings/classic → classicBot.yaml as JSON
|
|
13
|
+
* PUT /api/settings/fleet/defaults → merge fleet defaults
|
|
14
|
+
* PUT /api/settings/fleet/channels → replace channels[]
|
|
15
|
+
* POST /api/settings/fleet/instances/:name → create instance
|
|
16
|
+
* PATCH/api/settings/fleet/instances/:name → merge into instance
|
|
17
|
+
* DELETE /api/settings/fleet/instances/:name → remove instance
|
|
18
|
+
* PUT /api/settings/classic/defaults → merge classic defaults
|
|
19
|
+
* PATCH/api/settings/classic/channels/:key → update + restart a Classic channel
|
|
20
|
+
* POST /api/settings/reload → SIGHUP hot-reload
|
|
21
|
+
* POST /api/settings/instances/:name/pause → manually pause a running instance
|
|
22
|
+
* POST /api/settings/instances/:name/wake → manually wake a paused instance
|
|
23
|
+
* GET /api/settings/status-emojis → status-emoji keys, built-ins, pickable sets
|
|
24
|
+
* POST /api/settings/status-emojis/preview → resolve a map exactly as the bots will
|
|
25
|
+
* GET /api/settings/status-emojis/guild-emojis?channel=<id>[&refresh=1] → a Discord bot's servers' emojis
|
|
26
|
+
*
|
|
27
|
+
* Auth: all routes require the web.token — enforced by the global web-token gate
|
|
28
|
+
* in fleet-manager BEFORE this handler runs (settings paths are not exempt), so
|
|
29
|
+
* no per-route auth is repeated here.
|
|
30
|
+
*
|
|
31
|
+
* Writes are validated first (config-validator): any error → 400 and nothing is
|
|
32
|
+
* written; warnings are non-blocking and returned alongside the result.
|
|
33
|
+
*/
|
|
34
|
+
import { serveAppShell } from "./web-api.js";
|
|
35
|
+
import { isSettingsPage } from "./web-shell-routes.js";
|
|
1
36
|
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, statSync } from "node:fs";
|
|
2
37
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { join
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
38
|
+
import { join } from "node:path";
|
|
5
39
|
import yaml from "js-yaml";
|
|
6
40
|
import { KNOWN_BACKENDS, validateFleetConfig, validateClassicBotConfig } from "./config-validator.js";
|
|
7
41
|
import { clearPausedMarker } from "./pause-marker.js";
|
|
@@ -11,7 +45,6 @@ import { handleQuickstartRequest } from "./quickstart-api.js";
|
|
|
11
45
|
import { requestSessionBinding, } from "./connection-secrets.js";
|
|
12
46
|
import { providerRegistryEnvKeys, isReservedProviderEnvKey } from "./provider-secret-registry.js";
|
|
13
47
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, previewStatusEmojis, } from "./status-emojis.js";
|
|
14
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
48
|
function json(res, code, body, noStore = false) {
|
|
16
49
|
res.writeHead(code, {
|
|
17
50
|
"Content-Type": "application/json",
|
|
@@ -20,21 +53,7 @@ function json(res, code, body, noStore = false) {
|
|
|
20
53
|
res.end(JSON.stringify(body));
|
|
21
54
|
}
|
|
22
55
|
function readBody(req, maxBytes) {
|
|
23
|
-
return
|
|
24
|
-
const chunks = [];
|
|
25
|
-
let size = 0;
|
|
26
|
-
req.on("data", (c) => {
|
|
27
|
-
size += c.length;
|
|
28
|
-
if (size > maxBytes) {
|
|
29
|
-
reject(new Error("payload too large"));
|
|
30
|
-
req.destroy();
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
chunks.push(c);
|
|
34
|
-
});
|
|
35
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
36
|
-
req.on("error", reject);
|
|
37
|
-
});
|
|
56
|
+
return readBoundedWebBody(req, maxBytes);
|
|
38
57
|
}
|
|
39
58
|
/**
|
|
40
59
|
* Channel/group identifiers are opaque provider coordinates. JSON numbers
|
|
@@ -52,7 +71,7 @@ function normalizeChannelIdValue(value, path) {
|
|
|
52
71
|
return { ok: false, error: `${path} must be a string (or a safe integer)` };
|
|
53
72
|
}
|
|
54
73
|
export function isSettingsPath(path) {
|
|
55
|
-
return path
|
|
74
|
+
return isSettingsPage(path) || path.startsWith("/api/settings/");
|
|
56
75
|
}
|
|
57
76
|
const classicPath = (ctx) => join(ctx.dataDir, "classicBot.yaml");
|
|
58
77
|
class ClassicConfigParseError extends Error {
|
|
@@ -78,6 +97,7 @@ function writeClassicAtomic(ctx, classic) {
|
|
|
78
97
|
const target = classicPath(ctx);
|
|
79
98
|
const temp = `${target}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
80
99
|
const mode = existsSync(target) ? statSync(target).mode : 0o600;
|
|
100
|
+
noteSettingsWrite(target, readClassic(ctx), classic);
|
|
81
101
|
try {
|
|
82
102
|
writeFileSync(temp, yaml.dump(classic, { lineWidth: -1 }), { encoding: "utf-8", mode });
|
|
83
103
|
renameSync(temp, target);
|
|
@@ -98,6 +118,31 @@ const issueKey = (i) => i.path + "\u0000" + i.message;
|
|
|
98
118
|
* user out of saving anything, which reads as "save didn't persist".
|
|
99
119
|
* Returns true (and responds 400) when the edit adds errors.
|
|
100
120
|
*/
|
|
121
|
+
/**
|
|
122
|
+
* #1490 (Fable's 2.2 audit): the connection fields Settings writes. A PUT carries whole connections back, so a field
|
|
123
|
+
* outside these (one added by hand to fleet.yaml) passes through unchanged — but it is never added or changed here:
|
|
124
|
+
* an inline `bot_token`, or anything else, cannot reach fleet.yaml through this endpoint.
|
|
125
|
+
*/
|
|
126
|
+
const CHANNEL_FIELDS = new Set(["id", "type", "mode", "bot_token_env", "group_id", "access", "options", "telegram_api_root", "mirror_topic_id"]);
|
|
127
|
+
const CHANNEL_ACCESS_FIELDS = new Set(["mode", "allowed_users", "max_pending_codes", "code_expiry_minutes"]);
|
|
128
|
+
const CHANNEL_OPTION_FIELDS = new Set(["category_name", "general_channel_id", "topic_probe", "sticker_sets", "status_emojis"]);
|
|
129
|
+
function unownedChannelField(candidate, previous) {
|
|
130
|
+
const record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
131
|
+
const check = (fields, next, prev, at) => {
|
|
132
|
+
const before = record(prev) ? prev : {};
|
|
133
|
+
for (const key of Object.keys(next)) {
|
|
134
|
+
if (fields.has(key))
|
|
135
|
+
continue;
|
|
136
|
+
if (Object.hasOwn(before, key) && JSON.stringify(before[key]) === JSON.stringify(next[key]))
|
|
137
|
+
continue;
|
|
138
|
+
return `${at}${key}`;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
return check(CHANNEL_FIELDS, candidate, previous, "")
|
|
143
|
+
?? (record(candidate.access) ? check(CHANNEL_ACCESS_FIELDS, candidate.access, previous?.access, "access.") : null)
|
|
144
|
+
?? (record(candidate.options) ? check(CHANNEL_OPTION_FIELDS, candidate.options, previous?.options, "options.") : null);
|
|
145
|
+
}
|
|
101
146
|
function rejectIfWorse(res, before, after) {
|
|
102
147
|
const had = new Set(before.errors.map(issueKey));
|
|
103
148
|
const introduced = after.errors.filter(e => !had.has(issueKey(e)));
|
|
@@ -118,16 +163,13 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
118
163
|
if (!isSettingsPath(path))
|
|
119
164
|
return false;
|
|
120
165
|
const method = req.method ?? "GET";
|
|
121
|
-
// ──
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
res.end(html);
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
json(res, 500, { error: "settings.html not found" });
|
|
166
|
+
// ── The page: the app shell, which mounts the Settings panel (#1408 step 3) ──
|
|
167
|
+
if (isSettingsPage(path)) {
|
|
168
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
169
|
+
json(res, 405, { error: "Method not allowed" });
|
|
170
|
+
return true;
|
|
130
171
|
}
|
|
172
|
+
serveAppShell(req, res, ctx, "full");
|
|
131
173
|
return true;
|
|
132
174
|
}
|
|
133
175
|
// ── Reads ──
|
|
@@ -140,6 +182,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
140
182
|
// Non-null means a restart cannot clear the fleet row, so the page shows
|
|
141
183
|
// the mismatch instead of offering a button that can never succeed.
|
|
142
184
|
fleet_signature_mismatch: ctx.fleetSignatureMismatchKeys?.() ?? null,
|
|
185
|
+
// Whether the provider-secret routes exist on this fleet: the page reads them only then (no 404 on every load).
|
|
186
|
+
provider_secrets: ctx.providerSecretsEnabled?.() === true,
|
|
143
187
|
});
|
|
144
188
|
return true;
|
|
145
189
|
}
|
|
@@ -174,6 +218,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
174
218
|
}
|
|
175
219
|
if (method === "POST" && path === "/api/settings/status-emojis/preview") {
|
|
176
220
|
readBody(req, 64 * 1024).then(buf => {
|
|
221
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
222
|
+
return;
|
|
177
223
|
let body;
|
|
178
224
|
try {
|
|
179
225
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -279,6 +325,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
279
325
|
}
|
|
280
326
|
const specId = decodeURIComponent(providerSecretVerifyMatch[1]);
|
|
281
327
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
328
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
329
|
+
return;
|
|
282
330
|
let body;
|
|
283
331
|
try {
|
|
284
332
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -314,6 +362,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
314
362
|
}
|
|
315
363
|
const specId = decodeURIComponent(providerSecretApplyMatch[1]);
|
|
316
364
|
readBody(req, 16 * 1024).then(buf => {
|
|
365
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
366
|
+
return;
|
|
317
367
|
let body;
|
|
318
368
|
try {
|
|
319
369
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -328,7 +378,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
328
378
|
return json(res, 400, { error: "verification_id required" }, true);
|
|
329
379
|
if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
|
|
330
380
|
return json(res, 400, { error: "Idempotency-Key required" }, true);
|
|
331
|
-
const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key });
|
|
381
|
+
const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req) });
|
|
332
382
|
if ("error" in result)
|
|
333
383
|
return json(res, 422, { ok: false, error: "provider secret apply rejected" }, true);
|
|
334
384
|
if ("busy" in result)
|
|
@@ -364,6 +414,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
364
414
|
}
|
|
365
415
|
const connectionId = decodeURIComponent(secretVerifyMatch[1]);
|
|
366
416
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
417
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
418
|
+
return;
|
|
367
419
|
let body;
|
|
368
420
|
try {
|
|
369
421
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -407,6 +459,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
407
459
|
}
|
|
408
460
|
const connectionId = decodeURIComponent(secretApplyMatch[1]);
|
|
409
461
|
readBody(req, 16 * 1024).then(buf => {
|
|
462
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
463
|
+
return;
|
|
410
464
|
let body;
|
|
411
465
|
try {
|
|
412
466
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -425,7 +479,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
425
479
|
connectionId,
|
|
426
480
|
verificationId,
|
|
427
481
|
sessionBinding: requestSessionBinding(req),
|
|
428
|
-
idempotencyKey: key,
|
|
482
|
+
idempotencyKey: key, execution: settingsRequestExecution(req),
|
|
429
483
|
});
|
|
430
484
|
if ("error" in result)
|
|
431
485
|
return json(res, 422, { ok: false, error: "secret apply rejected" }, true);
|
|
@@ -468,6 +522,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
468
522
|
}
|
|
469
523
|
const connectionId = decodeURIComponent(bindingVerifyMatch[1]);
|
|
470
524
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
525
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
526
|
+
return;
|
|
471
527
|
let body;
|
|
472
528
|
try {
|
|
473
529
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -511,6 +567,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
511
567
|
}
|
|
512
568
|
const connectionId = decodeURIComponent(bindingApplyMatch[1]);
|
|
513
569
|
readBody(req, 16 * 1024).then(buf => {
|
|
570
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
571
|
+
return;
|
|
514
572
|
let body;
|
|
515
573
|
try {
|
|
516
574
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -526,7 +584,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
526
584
|
if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
|
|
527
585
|
return json(res, 400, { error: "Idempotency-Key required" }, true);
|
|
528
586
|
const result = ctx.startConnectionBindingApply({
|
|
529
|
-
connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key,
|
|
587
|
+
connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req),
|
|
530
588
|
});
|
|
531
589
|
if ("error" in result)
|
|
532
590
|
return json(res, 422, { ok: false, error: "binding apply rejected" }, true);
|
|
@@ -572,6 +630,53 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
572
630
|
.catch(err => json(res, 409, { error: err.message }));
|
|
573
631
|
return true;
|
|
574
632
|
}
|
|
633
|
+
// One hot web toggle; an unrelated Settings edit never materializes defaults.
|
|
634
|
+
if (method === "PUT" && path === "/api/settings/fleet/web") {
|
|
635
|
+
if (!cfg) {
|
|
636
|
+
json(res, 503, { error: "fleet not loaded" });
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
readBody(req, 4096).then(buf => {
|
|
640
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
641
|
+
return;
|
|
642
|
+
let body;
|
|
643
|
+
try {
|
|
644
|
+
body = JSON.parse(buf.toString("utf-8"));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return json(res, 400, { error: "invalid JSON" });
|
|
648
|
+
}
|
|
649
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
650
|
+
|| Object.keys(body).some(key => key !== "echo_to_channel" && key !== "public_link"))
|
|
651
|
+
return json(res, 400, { error: "unknown web field" });
|
|
652
|
+
const patch = body;
|
|
653
|
+
if (patch.echo_to_channel !== undefined && typeof patch.echo_to_channel !== "boolean")
|
|
654
|
+
return json(res, 400, { error: "echo_to_channel must be a boolean" });
|
|
655
|
+
if (patch.public_link !== undefined && !validPublicLinkPatch(patch.public_link))
|
|
656
|
+
return json(res, 400, { error: "invalid public_link settings" });
|
|
657
|
+
const changes = [];
|
|
658
|
+
const web = { ...cfg.web };
|
|
659
|
+
if (typeof patch.echo_to_channel === "boolean") {
|
|
660
|
+
web.echo_to_channel = patch.echo_to_channel;
|
|
661
|
+
changes.push({ path: ["web", "echo_to_channel"], value: patch.echo_to_channel });
|
|
662
|
+
}
|
|
663
|
+
if (validPublicLinkPatch(patch.public_link)) {
|
|
664
|
+
if (Object.keys(patch.public_link).length)
|
|
665
|
+
web.public_link = { ...web.public_link, ...patch.public_link };
|
|
666
|
+
for (const [key, value] of Object.entries(patch.public_link))
|
|
667
|
+
changes.push({ path: ["web", "public_link", key], value });
|
|
668
|
+
}
|
|
669
|
+
if (!changes.length)
|
|
670
|
+
return json(res, 200, { ok: true });
|
|
671
|
+
const before = validateFleetConfig(cfg);
|
|
672
|
+
const after = validateFleetConfig({ ...cfg, web });
|
|
673
|
+
if (rejectIfWorse(res, before, after))
|
|
674
|
+
return;
|
|
675
|
+
settingsWrite(req, () => { cfg.web = web; ctx.saveFleetConfig(changes); });
|
|
676
|
+
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
677
|
+
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
575
680
|
// ── Fleet defaults ──
|
|
576
681
|
if (method === "PUT" && path === "/api/settings/fleet/defaults") {
|
|
577
682
|
if (!cfg) {
|
|
@@ -579,6 +684,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
579
684
|
return true;
|
|
580
685
|
}
|
|
581
686
|
readBody(req, 512 * 1024).then(buf => {
|
|
687
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
688
|
+
return;
|
|
582
689
|
let body;
|
|
583
690
|
try {
|
|
584
691
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -597,8 +704,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
597
704
|
const after = validateFleetConfig({ ...cfg, defaults: merged });
|
|
598
705
|
if (rejectIfWorse(res, before, after))
|
|
599
706
|
return;
|
|
600
|
-
cfg.defaults = merged;
|
|
601
|
-
ctx.saveFleetConfig();
|
|
707
|
+
settingsWrite(req, () => { cfg.defaults = merged; ctx.saveFleetConfig(); });
|
|
602
708
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
603
709
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
604
710
|
return true;
|
|
@@ -610,6 +716,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
610
716
|
return true;
|
|
611
717
|
}
|
|
612
718
|
readBody(req, 512 * 1024).then(buf => {
|
|
719
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
720
|
+
return;
|
|
613
721
|
let body;
|
|
614
722
|
try {
|
|
615
723
|
body = JSON.parse(buf.toString("utf-8") || "[]");
|
|
@@ -650,6 +758,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
650
758
|
const id = typeof candidate.id === "string" ? candidate.id
|
|
651
759
|
: typeof candidate.type === "string" ? candidate.type : `channel-${index}`;
|
|
652
760
|
const previous = currentById.get(id);
|
|
761
|
+
const unowned = unownedChannelField(candidate, previous);
|
|
762
|
+
if (unowned)
|
|
763
|
+
return json(res, 400, { ok: false, error: `unsupported connection field: channels[${index}].${unowned}` }, true);
|
|
653
764
|
const tokenEnv = typeof candidate.bot_token_env === "string" ? candidate.bot_token_env : null;
|
|
654
765
|
if (tokenEnv && (providerRegistryEnvKeys().has(tokenEnv) || isReservedProviderEnvKey(tokenEnv))) {
|
|
655
766
|
return json(res, 409, { ok: false, error: "bot token env conflicts with a protected provider secret key" }, true);
|
|
@@ -680,9 +791,22 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
680
791
|
const after = validateFleetConfig(next);
|
|
681
792
|
if (rejectIfWorse(res, before, after))
|
|
682
793
|
return;
|
|
683
|
-
cfg.channels
|
|
684
|
-
|
|
685
|
-
|
|
794
|
+
const previous = { channels: cfg.channels, channel: cfg.channel, hadChannel: "channel" in cfg };
|
|
795
|
+
try {
|
|
796
|
+
settingsWrite(req, () => {
|
|
797
|
+
cfg.channels = normalizedBody;
|
|
798
|
+
delete cfg.channel;
|
|
799
|
+
ctx.saveFleetConfig();
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
// #1056: the save refused (fleet.yaml unchanged): memory goes back to what the file still says.
|
|
804
|
+
cfg.channels = previous.channels;
|
|
805
|
+
if (previous.hadChannel)
|
|
806
|
+
cfg.channel = previous.channel;
|
|
807
|
+
ctx.logger.warn({ err }, "settings: channels save refused");
|
|
808
|
+
return json(res, 500, { ok: false, error: err.message });
|
|
809
|
+
}
|
|
686
810
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
687
811
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
688
812
|
return true;
|
|
@@ -690,6 +814,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
690
814
|
// ── Classic defaults ──
|
|
691
815
|
if (method === "PUT" && path === "/api/settings/classic/defaults") {
|
|
692
816
|
readBody(req, 512 * 1024).then(buf => {
|
|
817
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
818
|
+
return;
|
|
693
819
|
let body;
|
|
694
820
|
try {
|
|
695
821
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -717,7 +843,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
717
843
|
return;
|
|
718
844
|
classic.defaults = merged;
|
|
719
845
|
try {
|
|
720
|
-
writeClassicAtomic(ctx, classic);
|
|
846
|
+
settingsWrite(req, () => writeClassicAtomic(ctx, classic));
|
|
721
847
|
}
|
|
722
848
|
catch (err) {
|
|
723
849
|
ctx.logger.warn({ err }, "settings: failed to atomically update classicBot.yaml");
|
|
@@ -744,6 +870,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
744
870
|
return true;
|
|
745
871
|
}
|
|
746
872
|
readBody(req, 512 * 1024).then(async (buf) => {
|
|
873
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
874
|
+
return;
|
|
747
875
|
let body;
|
|
748
876
|
try {
|
|
749
877
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -761,6 +889,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
761
889
|
"context_lines",
|
|
762
890
|
"tool_progress",
|
|
763
891
|
"reply_completion_guard",
|
|
892
|
+
"web_echo",
|
|
764
893
|
]);
|
|
765
894
|
const unknown = Object.keys(body).filter(field => !allowed.has(field));
|
|
766
895
|
if (unknown.length)
|
|
@@ -787,6 +916,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
787
916
|
&& typeof body.reply_completion_guard !== "boolean") {
|
|
788
917
|
return json(res, 400, { error: "reply_completion_guard must be a boolean" });
|
|
789
918
|
}
|
|
919
|
+
if (body.web_echo !== undefined && typeof body.web_echo !== "boolean") {
|
|
920
|
+
return json(res, 400, { error: "web_echo must be a boolean" });
|
|
921
|
+
}
|
|
790
922
|
let classic;
|
|
791
923
|
try {
|
|
792
924
|
classic = readClassic(ctx);
|
|
@@ -797,7 +929,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
797
929
|
const channels = classic.channels;
|
|
798
930
|
if (!channels || typeof channels !== "object" || Array.isArray(channels))
|
|
799
931
|
return json(res, 404, { error: "classic channel not found" });
|
|
800
|
-
const current = channels[key];
|
|
932
|
+
const current = Object.hasOwn(channels, key) ? channels[key] : undefined;
|
|
801
933
|
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
802
934
|
return json(res, 404, { error: "classic channel not found" });
|
|
803
935
|
const previous = structuredClone(classic);
|
|
@@ -816,29 +948,42 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
816
948
|
if (rejectIfWorse(res, before, after))
|
|
817
949
|
return;
|
|
818
950
|
try {
|
|
819
|
-
writeClassicAtomic(ctx, classic);
|
|
951
|
+
settingsWrite(req, () => writeClassicAtomic(ctx, classic), false);
|
|
820
952
|
}
|
|
821
953
|
catch (err) {
|
|
822
954
|
ctx.logger.warn({ err, key }, "settings: failed to atomically update classic channel");
|
|
823
955
|
return json(res, 500, { error: "failed to write classicBot.yaml" });
|
|
824
956
|
}
|
|
957
|
+
const undo = settingsUndo(classicPath(ctx), previous, classic, Object.keys(body).map(field => ["channels", key, field]));
|
|
958
|
+
const execution = settingsRequestExecution(req);
|
|
825
959
|
const instanceName = typeof merged.instanceName === "string" ? merged.instanceName : undefined;
|
|
960
|
+
const restore = instanceName ? ctx.captureClassicSettingsRestoration?.(instanceName, Object.keys(body), execution) : undefined;
|
|
826
961
|
try {
|
|
827
962
|
if (instanceName && ctx.restartClassicInstanceFromSettings) {
|
|
828
|
-
|
|
963
|
+
if (execution)
|
|
964
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body), execution);
|
|
965
|
+
else
|
|
966
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
|
|
967
|
+
execution?.assert();
|
|
829
968
|
}
|
|
830
969
|
}
|
|
831
970
|
catch (err) {
|
|
832
971
|
// Keep disk and runtime consistent if the requested restart fails.
|
|
833
972
|
try {
|
|
834
|
-
|
|
973
|
+
const retained = undoSettingsPaths(classicPath(ctx), readClassic(ctx), undo);
|
|
974
|
+
writeClassicAtomic(ctx, retained.value);
|
|
975
|
+
if (retained.conflicts)
|
|
976
|
+
throw new Error("concurrent Classic edit retained");
|
|
835
977
|
}
|
|
836
978
|
catch (rollbackErr) {
|
|
837
979
|
ctx.logger.error({ err: rollbackErr, key }, "settings: failed to roll back classic channel update");
|
|
838
980
|
}
|
|
839
981
|
if (instanceName && ctx.restartClassicInstanceFromSettings) {
|
|
840
982
|
try {
|
|
841
|
-
|
|
983
|
+
if (restore)
|
|
984
|
+
await restore();
|
|
985
|
+
else
|
|
986
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
|
|
842
987
|
}
|
|
843
988
|
catch (recoveryErr) {
|
|
844
989
|
ctx.logger.error({ err: recoveryErr, key, instanceName }, "settings: failed to restore classic instance after rollback");
|
|
@@ -847,6 +992,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
847
992
|
ctx.logger.warn({ err, key, instanceName }, "settings: classic channel restart failed; config rolled back");
|
|
848
993
|
return json(res, 409, { error: `classic instance restart failed: ${err.message}` });
|
|
849
994
|
}
|
|
995
|
+
execution?.complete();
|
|
850
996
|
ctx.logger.info({ key, instanceName }, "settings: updated classic channel");
|
|
851
997
|
const hotOnly = Object.keys(body).length > 0
|
|
852
998
|
&& Object.keys(body).every(field => CLASSIC_HOT_CONFIG_KEYS.has(field));
|
|
@@ -871,6 +1017,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
871
1017
|
return true;
|
|
872
1018
|
}
|
|
873
1019
|
readBody(req, 64 * 1024).then(buf => {
|
|
1020
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1021
|
+
return;
|
|
874
1022
|
let body = {};
|
|
875
1023
|
try {
|
|
876
1024
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -919,6 +1067,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
919
1067
|
return true;
|
|
920
1068
|
}
|
|
921
1069
|
readBody(req, 64 * 1024).then(async (buf) => {
|
|
1070
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1071
|
+
return;
|
|
922
1072
|
let body = {};
|
|
923
1073
|
try {
|
|
924
1074
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -983,9 +1133,6 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
983
1133
|
}
|
|
984
1134
|
// ── Instances (create / patch / delete) ──
|
|
985
1135
|
const validName = (n) => !!n && /^[^\\/\x00]+$/.test(n);
|
|
986
|
-
const nullableInstanceOverrides = new Set(["model", "auto_pause_after", "hang_detector", "agent_mode", "tool_set", "tool_progress", "reply_completion_guard", "log_level", "lightweight", "model_failover", "display_name", "status_emojis", "cross_instance_visibility"]);
|
|
987
|
-
const removesInstanceOverride = (key, value) => nullableInstanceOverrides.has(key)
|
|
988
|
-
&& (value === null || (key === "model" && typeof value === "string" && value.trim() === ""));
|
|
989
1136
|
const rawInstancePatches = (name, patch) => {
|
|
990
1137
|
const changes = [];
|
|
991
1138
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -1008,33 +1155,24 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1008
1155
|
}
|
|
1009
1156
|
const base = (exists ? cfg.instances[name] : {});
|
|
1010
1157
|
const patch = body;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const mergedHang = { ...(base.hang_detector ?? {}), ...hangPatch };
|
|
1015
|
-
// Nested null removes only the timeout override while preserving any
|
|
1016
|
-
// independently configured `enabled` override.
|
|
1017
|
-
if (hangPatch.timeout_minutes === null)
|
|
1018
|
-
delete mergedHang.timeout_minutes;
|
|
1019
|
-
if (Object.keys(mergedHang).length)
|
|
1020
|
-
mergedInst.hang_detector = mergedHang;
|
|
1021
|
-
else
|
|
1022
|
-
delete mergedInst.hang_detector;
|
|
1158
|
+
let mergedInst;
|
|
1159
|
+
try {
|
|
1160
|
+
mergedInst = normalizeSettingsInstancePatch(base, patch);
|
|
1023
1161
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (removesInstanceOverride(key, patch[key]))
|
|
1028
|
-
delete mergedInst[key];
|
|
1162
|
+
catch {
|
|
1163
|
+
json(res, 400, { error: "unsupported_instance_null" });
|
|
1164
|
+
return;
|
|
1029
1165
|
}
|
|
1030
1166
|
const before = validateFleetConfig(cfg);
|
|
1031
1167
|
const after = validateFleetConfig({ ...cfg, instances: { ...cfg.instances, [name]: mergedInst } });
|
|
1032
1168
|
if (rejectIfWorse(res, before, after))
|
|
1033
1169
|
return;
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1170
|
+
settingsWrite(req, () => {
|
|
1171
|
+
Object.defineProperty(cfg.instances, name, { value: mergedInst, writable: true, enumerable: true, configurable: true });
|
|
1172
|
+
if (!exists)
|
|
1173
|
+
clearPausedMarker(join(ctx.dataDir, "instances", name));
|
|
1174
|
+
ctx.saveFleetConfig(rawInstancePatches(name, patch));
|
|
1175
|
+
});
|
|
1038
1176
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
1039
1177
|
};
|
|
1040
1178
|
// POST /api/settings/fleet/instances — create, name taken from the body.
|
|
@@ -1044,6 +1182,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1044
1182
|
return true;
|
|
1045
1183
|
}
|
|
1046
1184
|
readBody(req, 512 * 1024).then(buf => {
|
|
1185
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1186
|
+
return;
|
|
1047
1187
|
let body;
|
|
1048
1188
|
try {
|
|
1049
1189
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -1056,7 +1196,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1056
1196
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1057
1197
|
if (!validName(name))
|
|
1058
1198
|
return json(res, 400, { error: "missing or invalid instance name (provide `name` in the body)" });
|
|
1059
|
-
if (cfg.instances
|
|
1199
|
+
if (Object.hasOwn(cfg.instances, name))
|
|
1060
1200
|
return json(res, 409, { error: "instance already exists" });
|
|
1061
1201
|
const { name: _n, ...instBody } = body;
|
|
1062
1202
|
commitInstance(name, false, instBody);
|
|
@@ -1076,19 +1216,21 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1076
1216
|
return true;
|
|
1077
1217
|
}
|
|
1078
1218
|
if (method === "DELETE") {
|
|
1079
|
-
if (!cfg.instances
|
|
1219
|
+
if (!Object.hasOwn(cfg.instances, name)) {
|
|
1080
1220
|
json(res, 404, { error: "instance not found" });
|
|
1081
1221
|
return true;
|
|
1082
1222
|
}
|
|
1083
1223
|
// DELETE never blocks on validation; surface any resulting warnings.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1224
|
+
settingsWrite(req, () => {
|
|
1225
|
+
delete cfg.instances[name];
|
|
1226
|
+
clearPausedMarker(join(ctx.dataDir, "instances", name));
|
|
1227
|
+
ctx.saveFleetConfig();
|
|
1228
|
+
});
|
|
1087
1229
|
json(res, 200, { ok: true, warnings: validateFleetConfig(cfg).warnings });
|
|
1088
1230
|
return true;
|
|
1089
1231
|
}
|
|
1090
1232
|
if (method === "POST" || method === "PATCH") {
|
|
1091
|
-
const exists =
|
|
1233
|
+
const exists = Object.hasOwn(cfg.instances, name);
|
|
1092
1234
|
if (method === "POST" && exists) {
|
|
1093
1235
|
json(res, 409, { error: "instance already exists" });
|
|
1094
1236
|
return true;
|
|
@@ -1098,6 +1240,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1098
1240
|
return true;
|
|
1099
1241
|
}
|
|
1100
1242
|
readBody(req, 512 * 1024).then(buf => {
|
|
1243
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1244
|
+
return;
|
|
1101
1245
|
let body;
|
|
1102
1246
|
try {
|
|
1103
1247
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|