@songsid/agend 2.1.12 → 2.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- 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/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/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 +83 -7
- package/dist/backend/kiro.js +418 -91
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js.map +1 -1
- 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.js +225 -127
- 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 +89 -4
- package/dist/daemon.js +661 -77
- 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-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-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +6 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +293 -12
- package/dist/fleet-manager.js +2364 -449
- package/dist/fleet-manager.js.map +1 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +5 -0
- 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 +150 -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/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 +6 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +38 -0
- 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 +110 -0
- package/dist/service-activation.js +302 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +7 -4
- package/dist/service-installer.js.map +1 -1
- 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 +175 -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 +133 -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 +197 -32
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +3 -0
- 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/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 +35 -5
- package/dist/tmux-control.js +291 -57
- 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 +191 -58
- 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-render.js +478 -0
- package/dist/ui/chat-store.js +289 -0
- package/dist/ui/chat-strings.js +69 -0
- package/dist/ui/chat-thread.js +209 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +418 -0
- package/dist/ui/panel-fleet.js +413 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +750 -0
- package/dist/ui/preview.js +266 -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 +652 -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 +344 -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 +676 -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 +130 -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 +61 -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 +51 -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-install.d.ts +71 -0
- package/dist/update-install.js +162 -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 +71 -14
- package/dist/web-api.js +492 -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-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/package.json +8 -5
- package/scripts/preinstall-guard.cjs +36 -0
- 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);
|
|
@@ -118,16 +138,13 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
118
138
|
if (!isSettingsPath(path))
|
|
119
139
|
return false;
|
|
120
140
|
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" });
|
|
141
|
+
// ── The page: the app shell, which mounts the Settings panel (#1408 step 3) ──
|
|
142
|
+
if (isSettingsPage(path)) {
|
|
143
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
144
|
+
json(res, 405, { error: "Method not allowed" });
|
|
145
|
+
return true;
|
|
130
146
|
}
|
|
147
|
+
serveAppShell(req, res, ctx, "full");
|
|
131
148
|
return true;
|
|
132
149
|
}
|
|
133
150
|
// ── Reads ──
|
|
@@ -174,6 +191,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
174
191
|
}
|
|
175
192
|
if (method === "POST" && path === "/api/settings/status-emojis/preview") {
|
|
176
193
|
readBody(req, 64 * 1024).then(buf => {
|
|
194
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
195
|
+
return;
|
|
177
196
|
let body;
|
|
178
197
|
try {
|
|
179
198
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -279,6 +298,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
279
298
|
}
|
|
280
299
|
const specId = decodeURIComponent(providerSecretVerifyMatch[1]);
|
|
281
300
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
301
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
302
|
+
return;
|
|
282
303
|
let body;
|
|
283
304
|
try {
|
|
284
305
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -314,6 +335,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
314
335
|
}
|
|
315
336
|
const specId = decodeURIComponent(providerSecretApplyMatch[1]);
|
|
316
337
|
readBody(req, 16 * 1024).then(buf => {
|
|
338
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
339
|
+
return;
|
|
317
340
|
let body;
|
|
318
341
|
try {
|
|
319
342
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -328,7 +351,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
328
351
|
return json(res, 400, { error: "verification_id required" }, true);
|
|
329
352
|
if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
|
|
330
353
|
return json(res, 400, { error: "Idempotency-Key required" }, true);
|
|
331
|
-
const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key });
|
|
354
|
+
const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req) });
|
|
332
355
|
if ("error" in result)
|
|
333
356
|
return json(res, 422, { ok: false, error: "provider secret apply rejected" }, true);
|
|
334
357
|
if ("busy" in result)
|
|
@@ -364,6 +387,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
364
387
|
}
|
|
365
388
|
const connectionId = decodeURIComponent(secretVerifyMatch[1]);
|
|
366
389
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
390
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
391
|
+
return;
|
|
367
392
|
let body;
|
|
368
393
|
try {
|
|
369
394
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -407,6 +432,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
407
432
|
}
|
|
408
433
|
const connectionId = decodeURIComponent(secretApplyMatch[1]);
|
|
409
434
|
readBody(req, 16 * 1024).then(buf => {
|
|
435
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
436
|
+
return;
|
|
410
437
|
let body;
|
|
411
438
|
try {
|
|
412
439
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -425,7 +452,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
425
452
|
connectionId,
|
|
426
453
|
verificationId,
|
|
427
454
|
sessionBinding: requestSessionBinding(req),
|
|
428
|
-
idempotencyKey: key,
|
|
455
|
+
idempotencyKey: key, execution: settingsRequestExecution(req),
|
|
429
456
|
});
|
|
430
457
|
if ("error" in result)
|
|
431
458
|
return json(res, 422, { ok: false, error: "secret apply rejected" }, true);
|
|
@@ -468,6 +495,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
468
495
|
}
|
|
469
496
|
const connectionId = decodeURIComponent(bindingVerifyMatch[1]);
|
|
470
497
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
498
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
499
|
+
return;
|
|
471
500
|
let body;
|
|
472
501
|
try {
|
|
473
502
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -511,6 +540,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
511
540
|
}
|
|
512
541
|
const connectionId = decodeURIComponent(bindingApplyMatch[1]);
|
|
513
542
|
readBody(req, 16 * 1024).then(buf => {
|
|
543
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
544
|
+
return;
|
|
514
545
|
let body;
|
|
515
546
|
try {
|
|
516
547
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -526,7 +557,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
526
557
|
if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
|
|
527
558
|
return json(res, 400, { error: "Idempotency-Key required" }, true);
|
|
528
559
|
const result = ctx.startConnectionBindingApply({
|
|
529
|
-
connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key,
|
|
560
|
+
connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req),
|
|
530
561
|
});
|
|
531
562
|
if ("error" in result)
|
|
532
563
|
return json(res, 422, { ok: false, error: "binding apply rejected" }, true);
|
|
@@ -572,6 +603,53 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
572
603
|
.catch(err => json(res, 409, { error: err.message }));
|
|
573
604
|
return true;
|
|
574
605
|
}
|
|
606
|
+
// One hot web toggle; an unrelated Settings edit never materializes defaults.
|
|
607
|
+
if (method === "PUT" && path === "/api/settings/fleet/web") {
|
|
608
|
+
if (!cfg) {
|
|
609
|
+
json(res, 503, { error: "fleet not loaded" });
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
readBody(req, 4096).then(buf => {
|
|
613
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
614
|
+
return;
|
|
615
|
+
let body;
|
|
616
|
+
try {
|
|
617
|
+
body = JSON.parse(buf.toString("utf-8"));
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
return json(res, 400, { error: "invalid JSON" });
|
|
621
|
+
}
|
|
622
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
623
|
+
|| Object.keys(body).some(key => key !== "echo_to_channel" && key !== "public_link"))
|
|
624
|
+
return json(res, 400, { error: "unknown web field" });
|
|
625
|
+
const patch = body;
|
|
626
|
+
if (patch.echo_to_channel !== undefined && typeof patch.echo_to_channel !== "boolean")
|
|
627
|
+
return json(res, 400, { error: "echo_to_channel must be a boolean" });
|
|
628
|
+
if (patch.public_link !== undefined && !validPublicLinkPatch(patch.public_link))
|
|
629
|
+
return json(res, 400, { error: "invalid public_link settings" });
|
|
630
|
+
const changes = [];
|
|
631
|
+
const web = { ...cfg.web };
|
|
632
|
+
if (typeof patch.echo_to_channel === "boolean") {
|
|
633
|
+
web.echo_to_channel = patch.echo_to_channel;
|
|
634
|
+
changes.push({ path: ["web", "echo_to_channel"], value: patch.echo_to_channel });
|
|
635
|
+
}
|
|
636
|
+
if (validPublicLinkPatch(patch.public_link)) {
|
|
637
|
+
if (Object.keys(patch.public_link).length)
|
|
638
|
+
web.public_link = { ...web.public_link, ...patch.public_link };
|
|
639
|
+
for (const [key, value] of Object.entries(patch.public_link))
|
|
640
|
+
changes.push({ path: ["web", "public_link", key], value });
|
|
641
|
+
}
|
|
642
|
+
if (!changes.length)
|
|
643
|
+
return json(res, 200, { ok: true });
|
|
644
|
+
const before = validateFleetConfig(cfg);
|
|
645
|
+
const after = validateFleetConfig({ ...cfg, web });
|
|
646
|
+
if (rejectIfWorse(res, before, after))
|
|
647
|
+
return;
|
|
648
|
+
settingsWrite(req, () => { cfg.web = web; ctx.saveFleetConfig(changes); });
|
|
649
|
+
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
650
|
+
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
575
653
|
// ── Fleet defaults ──
|
|
576
654
|
if (method === "PUT" && path === "/api/settings/fleet/defaults") {
|
|
577
655
|
if (!cfg) {
|
|
@@ -579,6 +657,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
579
657
|
return true;
|
|
580
658
|
}
|
|
581
659
|
readBody(req, 512 * 1024).then(buf => {
|
|
660
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
661
|
+
return;
|
|
582
662
|
let body;
|
|
583
663
|
try {
|
|
584
664
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -597,8 +677,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
597
677
|
const after = validateFleetConfig({ ...cfg, defaults: merged });
|
|
598
678
|
if (rejectIfWorse(res, before, after))
|
|
599
679
|
return;
|
|
600
|
-
cfg.defaults = merged;
|
|
601
|
-
ctx.saveFleetConfig();
|
|
680
|
+
settingsWrite(req, () => { cfg.defaults = merged; ctx.saveFleetConfig(); });
|
|
602
681
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
603
682
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
604
683
|
return true;
|
|
@@ -610,6 +689,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
610
689
|
return true;
|
|
611
690
|
}
|
|
612
691
|
readBody(req, 512 * 1024).then(buf => {
|
|
692
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
693
|
+
return;
|
|
613
694
|
let body;
|
|
614
695
|
try {
|
|
615
696
|
body = JSON.parse(buf.toString("utf-8") || "[]");
|
|
@@ -680,9 +761,11 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
680
761
|
const after = validateFleetConfig(next);
|
|
681
762
|
if (rejectIfWorse(res, before, after))
|
|
682
763
|
return;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
764
|
+
settingsWrite(req, () => {
|
|
765
|
+
cfg.channels = normalizedBody;
|
|
766
|
+
delete cfg.channel;
|
|
767
|
+
ctx.saveFleetConfig();
|
|
768
|
+
});
|
|
686
769
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
687
770
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
688
771
|
return true;
|
|
@@ -690,6 +773,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
690
773
|
// ── Classic defaults ──
|
|
691
774
|
if (method === "PUT" && path === "/api/settings/classic/defaults") {
|
|
692
775
|
readBody(req, 512 * 1024).then(buf => {
|
|
776
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
777
|
+
return;
|
|
693
778
|
let body;
|
|
694
779
|
try {
|
|
695
780
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -717,7 +802,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
717
802
|
return;
|
|
718
803
|
classic.defaults = merged;
|
|
719
804
|
try {
|
|
720
|
-
writeClassicAtomic(ctx, classic);
|
|
805
|
+
settingsWrite(req, () => writeClassicAtomic(ctx, classic));
|
|
721
806
|
}
|
|
722
807
|
catch (err) {
|
|
723
808
|
ctx.logger.warn({ err }, "settings: failed to atomically update classicBot.yaml");
|
|
@@ -744,6 +829,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
744
829
|
return true;
|
|
745
830
|
}
|
|
746
831
|
readBody(req, 512 * 1024).then(async (buf) => {
|
|
832
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
833
|
+
return;
|
|
747
834
|
let body;
|
|
748
835
|
try {
|
|
749
836
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -761,6 +848,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
761
848
|
"context_lines",
|
|
762
849
|
"tool_progress",
|
|
763
850
|
"reply_completion_guard",
|
|
851
|
+
"web_echo",
|
|
764
852
|
]);
|
|
765
853
|
const unknown = Object.keys(body).filter(field => !allowed.has(field));
|
|
766
854
|
if (unknown.length)
|
|
@@ -787,6 +875,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
787
875
|
&& typeof body.reply_completion_guard !== "boolean") {
|
|
788
876
|
return json(res, 400, { error: "reply_completion_guard must be a boolean" });
|
|
789
877
|
}
|
|
878
|
+
if (body.web_echo !== undefined && typeof body.web_echo !== "boolean") {
|
|
879
|
+
return json(res, 400, { error: "web_echo must be a boolean" });
|
|
880
|
+
}
|
|
790
881
|
let classic;
|
|
791
882
|
try {
|
|
792
883
|
classic = readClassic(ctx);
|
|
@@ -797,7 +888,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
797
888
|
const channels = classic.channels;
|
|
798
889
|
if (!channels || typeof channels !== "object" || Array.isArray(channels))
|
|
799
890
|
return json(res, 404, { error: "classic channel not found" });
|
|
800
|
-
const current = channels[key];
|
|
891
|
+
const current = Object.hasOwn(channels, key) ? channels[key] : undefined;
|
|
801
892
|
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
802
893
|
return json(res, 404, { error: "classic channel not found" });
|
|
803
894
|
const previous = structuredClone(classic);
|
|
@@ -816,29 +907,42 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
816
907
|
if (rejectIfWorse(res, before, after))
|
|
817
908
|
return;
|
|
818
909
|
try {
|
|
819
|
-
writeClassicAtomic(ctx, classic);
|
|
910
|
+
settingsWrite(req, () => writeClassicAtomic(ctx, classic), false);
|
|
820
911
|
}
|
|
821
912
|
catch (err) {
|
|
822
913
|
ctx.logger.warn({ err, key }, "settings: failed to atomically update classic channel");
|
|
823
914
|
return json(res, 500, { error: "failed to write classicBot.yaml" });
|
|
824
915
|
}
|
|
916
|
+
const undo = settingsUndo(classicPath(ctx), previous, classic, Object.keys(body).map(field => ["channels", key, field]));
|
|
917
|
+
const execution = settingsRequestExecution(req);
|
|
825
918
|
const instanceName = typeof merged.instanceName === "string" ? merged.instanceName : undefined;
|
|
919
|
+
const restore = instanceName ? ctx.captureClassicSettingsRestoration?.(instanceName, Object.keys(body), execution) : undefined;
|
|
826
920
|
try {
|
|
827
921
|
if (instanceName && ctx.restartClassicInstanceFromSettings) {
|
|
828
|
-
|
|
922
|
+
if (execution)
|
|
923
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body), execution);
|
|
924
|
+
else
|
|
925
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
|
|
926
|
+
execution?.assert();
|
|
829
927
|
}
|
|
830
928
|
}
|
|
831
929
|
catch (err) {
|
|
832
930
|
// Keep disk and runtime consistent if the requested restart fails.
|
|
833
931
|
try {
|
|
834
|
-
|
|
932
|
+
const retained = undoSettingsPaths(classicPath(ctx), readClassic(ctx), undo);
|
|
933
|
+
writeClassicAtomic(ctx, retained.value);
|
|
934
|
+
if (retained.conflicts)
|
|
935
|
+
throw new Error("concurrent Classic edit retained");
|
|
835
936
|
}
|
|
836
937
|
catch (rollbackErr) {
|
|
837
938
|
ctx.logger.error({ err: rollbackErr, key }, "settings: failed to roll back classic channel update");
|
|
838
939
|
}
|
|
839
940
|
if (instanceName && ctx.restartClassicInstanceFromSettings) {
|
|
840
941
|
try {
|
|
841
|
-
|
|
942
|
+
if (restore)
|
|
943
|
+
await restore();
|
|
944
|
+
else
|
|
945
|
+
await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
|
|
842
946
|
}
|
|
843
947
|
catch (recoveryErr) {
|
|
844
948
|
ctx.logger.error({ err: recoveryErr, key, instanceName }, "settings: failed to restore classic instance after rollback");
|
|
@@ -847,6 +951,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
847
951
|
ctx.logger.warn({ err, key, instanceName }, "settings: classic channel restart failed; config rolled back");
|
|
848
952
|
return json(res, 409, { error: `classic instance restart failed: ${err.message}` });
|
|
849
953
|
}
|
|
954
|
+
execution?.complete();
|
|
850
955
|
ctx.logger.info({ key, instanceName }, "settings: updated classic channel");
|
|
851
956
|
const hotOnly = Object.keys(body).length > 0
|
|
852
957
|
&& Object.keys(body).every(field => CLASSIC_HOT_CONFIG_KEYS.has(field));
|
|
@@ -871,6 +976,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
871
976
|
return true;
|
|
872
977
|
}
|
|
873
978
|
readBody(req, 64 * 1024).then(buf => {
|
|
979
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
980
|
+
return;
|
|
874
981
|
let body = {};
|
|
875
982
|
try {
|
|
876
983
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -919,6 +1026,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
919
1026
|
return true;
|
|
920
1027
|
}
|
|
921
1028
|
readBody(req, 64 * 1024).then(async (buf) => {
|
|
1029
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1030
|
+
return;
|
|
922
1031
|
let body = {};
|
|
923
1032
|
try {
|
|
924
1033
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -983,9 +1092,6 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
983
1092
|
}
|
|
984
1093
|
// ── Instances (create / patch / delete) ──
|
|
985
1094
|
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
1095
|
const rawInstancePatches = (name, patch) => {
|
|
990
1096
|
const changes = [];
|
|
991
1097
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -1008,33 +1114,24 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1008
1114
|
}
|
|
1009
1115
|
const base = (exists ? cfg.instances[name] : {});
|
|
1010
1116
|
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;
|
|
1117
|
+
let mergedInst;
|
|
1118
|
+
try {
|
|
1119
|
+
mergedInst = normalizeSettingsInstancePatch(base, patch);
|
|
1023
1120
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (removesInstanceOverride(key, patch[key]))
|
|
1028
|
-
delete mergedInst[key];
|
|
1121
|
+
catch {
|
|
1122
|
+
json(res, 400, { error: "unsupported_instance_null" });
|
|
1123
|
+
return;
|
|
1029
1124
|
}
|
|
1030
1125
|
const before = validateFleetConfig(cfg);
|
|
1031
1126
|
const after = validateFleetConfig({ ...cfg, instances: { ...cfg.instances, [name]: mergedInst } });
|
|
1032
1127
|
if (rejectIfWorse(res, before, after))
|
|
1033
1128
|
return;
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1129
|
+
settingsWrite(req, () => {
|
|
1130
|
+
Object.defineProperty(cfg.instances, name, { value: mergedInst, writable: true, enumerable: true, configurable: true });
|
|
1131
|
+
if (!exists)
|
|
1132
|
+
clearPausedMarker(join(ctx.dataDir, "instances", name));
|
|
1133
|
+
ctx.saveFleetConfig(rawInstancePatches(name, patch));
|
|
1134
|
+
});
|
|
1038
1135
|
json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
|
|
1039
1136
|
};
|
|
1040
1137
|
// POST /api/settings/fleet/instances — create, name taken from the body.
|
|
@@ -1044,6 +1141,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1044
1141
|
return true;
|
|
1045
1142
|
}
|
|
1046
1143
|
readBody(req, 512 * 1024).then(buf => {
|
|
1144
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1145
|
+
return;
|
|
1047
1146
|
let body;
|
|
1048
1147
|
try {
|
|
1049
1148
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|
|
@@ -1056,7 +1155,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1056
1155
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1057
1156
|
if (!validName(name))
|
|
1058
1157
|
return json(res, 400, { error: "missing or invalid instance name (provide `name` in the body)" });
|
|
1059
|
-
if (cfg.instances
|
|
1158
|
+
if (Object.hasOwn(cfg.instances, name))
|
|
1060
1159
|
return json(res, 409, { error: "instance already exists" });
|
|
1061
1160
|
const { name: _n, ...instBody } = body;
|
|
1062
1161
|
commitInstance(name, false, instBody);
|
|
@@ -1076,19 +1175,21 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1076
1175
|
return true;
|
|
1077
1176
|
}
|
|
1078
1177
|
if (method === "DELETE") {
|
|
1079
|
-
if (!cfg.instances
|
|
1178
|
+
if (!Object.hasOwn(cfg.instances, name)) {
|
|
1080
1179
|
json(res, 404, { error: "instance not found" });
|
|
1081
1180
|
return true;
|
|
1082
1181
|
}
|
|
1083
1182
|
// DELETE never blocks on validation; surface any resulting warnings.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1183
|
+
settingsWrite(req, () => {
|
|
1184
|
+
delete cfg.instances[name];
|
|
1185
|
+
clearPausedMarker(join(ctx.dataDir, "instances", name));
|
|
1186
|
+
ctx.saveFleetConfig();
|
|
1187
|
+
});
|
|
1087
1188
|
json(res, 200, { ok: true, warnings: validateFleetConfig(cfg).warnings });
|
|
1088
1189
|
return true;
|
|
1089
1190
|
}
|
|
1090
1191
|
if (method === "POST" || method === "PATCH") {
|
|
1091
|
-
const exists =
|
|
1192
|
+
const exists = Object.hasOwn(cfg.instances, name);
|
|
1092
1193
|
if (method === "POST" && exists) {
|
|
1093
1194
|
json(res, 409, { error: "instance already exists" });
|
|
1094
1195
|
return true;
|
|
@@ -1098,6 +1199,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
|
|
|
1098
1199
|
return true;
|
|
1099
1200
|
}
|
|
1100
1201
|
readBody(req, 512 * 1024).then(buf => {
|
|
1202
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1203
|
+
return;
|
|
1101
1204
|
let body;
|
|
1102
1205
|
try {
|
|
1103
1206
|
body = JSON.parse(buf.toString("utf-8") || "{}");
|