@songsid/agend 2.3.0-beta.21 → 2.3.0-beta.23
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/agent-cli.js +4 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +9 -0
- package/dist/agent-endpoint.js +15 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +5 -0
- package/dist/agent-whats-new.js +16 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +7 -0
- package/dist/auth-api.js +10 -3
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex-package.d.ts +30 -0
- package/dist/backend/codex-package.js +133 -0
- package/dist/backend/codex-package.js.map +1 -0
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +9 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +56 -3
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -1
- package/dist/backend/kiro.js +102 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode-permission.js +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js +2 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/binary-probe.js +1 -1
- package/dist/bug-report-cli.js +1 -1
- package/dist/bundled-skills.d.ts +7 -3
- package/dist/bundled-skills.js +16 -7
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +12 -4
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +3 -1
- package/dist/channel/mcp-tools.js +38 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +54 -0
- package/dist/child-process.js.map +1 -0
- package/dist/cli.js +167 -182
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +28 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +16 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +14 -0
- package/dist/daemon.js +45 -8
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.js +1 -1
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +42 -0
- package/dist/doctor.js.map +1 -1
- package/dist/export-import.js +1 -1
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +73 -2
- package/dist/fleet-manager.js +400 -33
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.d.ts +5 -5
- package/dist/full-restart.js +5 -5
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +8 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +19 -4
- package/dist/general-knowledge/steering/core-rules.md +1 -1
- package/dist/instance-config-impact.js +13 -3
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +6 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +137 -10
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.js +1 -1
- package/dist/login-shell.js +3 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/ls-rows.js +1 -1
- package/dist/needs-you-hub.d.ts +98 -1
- package/dist/needs-you-hub.js +313 -44
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +5 -2
- package/dist/needs-you-live.js +6 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +58 -3
- package/dist/needs-you.js +36 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +25 -10
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -3
- package/dist/provider-secret-registry.d.ts +5 -3
- package/dist/provider-secret-registry.js +8 -2
- package/dist/provider-secret-registry.js.map +1 -1
- package/dist/public-link-progress.d.ts +12 -1
- package/dist/public-link-progress.js +5 -10
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +11 -0
- package/dist/public-tunnel-config.js +68 -0
- package/dist/public-tunnel-config.js.map +1 -0
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +3 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +13 -2
- package/dist/public-web-link.js +37 -10
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart.js +14 -8
- package/dist/quickstart.js.map +1 -1
- package/dist/resource-report.js +1 -1
- package/dist/scheduler/task-board.d.ts +41 -1
- package/dist/scheduler/task-board.js +150 -4
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.js +2 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/secret-store.d.ts +2 -0
- package/dist/secret-store.js +21 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-installer.d.ts +69 -0
- package/dist/service-installer.js +181 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +44 -0
- package/dist/service-owner.js +48 -0
- package/dist/service-owner.js.map +1 -0
- package/dist/settings-api.d.ts +5 -0
- package/dist/settings-api.js +32 -10
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +86 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +6 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +2 -0
- package/dist/settings-http-confirmation.js +4 -3
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +4 -1
- package/dist/settings-request-capability.js +11 -3
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/setup-host.js +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-summary.d.ts +101 -0
- package/dist/task-board-summary.js +177 -0
- package/dist/task-board-summary.js.map +1 -0
- package/dist/task-board-waker.d.ts +8 -1
- package/dist/task-board-waker.js +15 -6
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.js +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +5 -1
- package/dist/tmux-exec-helper.js +13 -21
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.js +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -0
- package/dist/topic-commands.js +29 -18
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +5 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +2 -0
- package/dist/transcript-sources.js +4 -0
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js +3 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -16
- package/dist/tunnel/cloudflared.js +65 -13
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +13 -0
- package/dist/tunnel/named-diagnostics.js +52 -0
- package/dist/tunnel/named-diagnostics.js.map +1 -0
- package/dist/tunnel/types.d.ts +2 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/ui/app-effects.js +10 -4
- package/dist/ui/app.html +2 -0
- package/dist/ui/chat-render.js +39 -0
- package/dist/ui/chat-store.js +8 -0
- package/dist/ui/chat-strings.js +5 -2
- package/dist/ui/chat-thread.js +15 -8
- package/dist/ui/effect-trial-offer.js +2 -31
- package/dist/ui/effects-waker.js +2 -70
- package/dist/ui/fleet-canvas.js +14 -6
- package/dist/ui/fleet-route-scene.js +86 -21
- package/dist/ui/fleet-task-card.js +5 -5
- package/dist/ui/fleet-task-common.js +41 -7
- package/dist/ui/fleet-task-work.js +107 -0
- package/dist/ui/fleet-tasks.js +46 -12
- package/dist/ui/fleet-traffic.js +57 -10
- package/dist/ui/icons/icon-192.png +0 -0
- package/dist/ui/icons/icon-512.png +0 -0
- package/dist/ui/image-lightbox.js +39 -1
- package/dist/ui/image-zoom.js +242 -0
- package/dist/ui/panel-chat.js +24 -13
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +6 -2
- package/dist/ui/panel-needs.js +21 -6
- package/dist/ui/panel-settings.js +90 -15
- package/dist/ui/preview-panel.js +2 -1
- package/dist/ui/search-dialog.js +2 -1
- package/dist/ui/settings-appearance.js +159 -9
- package/dist/ui/settings-dialogs.js +33 -17
- package/dist/ui/settings-discord.js +6 -2
- package/dist/ui/settings-guests.js +2 -1
- package/dist/ui/settings-learn.js +1 -0
- package/dist/ui/settings-model.js +2 -11
- package/dist/ui/settings-mods.js +6 -3
- package/dist/ui/settings-strings.js +80 -0
- package/dist/ui/settings-token.js +3 -1
- package/dist/ui/settings-web-authority.js +6 -3
- package/dist/ui/settings-wizard.js +13 -7
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +2 -2
- package/dist/ui/shared/app-store.js +2 -2
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +125 -28
- package/dist/ui/shared/app.js +5 -1
- package/dist/ui/shared/effect-app-runtime.js +7 -5
- package/dist/ui/shared/effect-chat-runtime.js +2 -7
- package/dist/ui/shared/effect-cursor.js +3 -2
- package/dist/ui/shared/effect-display-metadata.js +39 -0
- package/dist/ui/shared/effect-registry.js +4 -2
- package/dist/ui/shared/effect-runtime-loader.js +3 -1
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/feature-flags.js +127 -0
- package/dist/ui/shared/identity-color.js +6 -3
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/text-undo.js +210 -0
- package/dist/ui/shared/ui-dialog.js +4 -3
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/signin.html +2 -0
- package/dist/update-dispatch.js +1 -1
- package/dist/update-launch.d.ts +1 -1
- package/dist/update-launch.js +1 -2
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.js +1 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/web-api.d.ts +12 -2
- package/dist/web-api.js +266 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon-worker.d.ts +1 -0
- package/dist/web-app-icon-worker.js +15 -0
- package/dist/web-app-icon-worker.js.map +1 -0
- package/dist/web-app-icon.d.ts +107 -0
- package/dist/web-app-icon.js +256 -0
- package/dist/web-app-icon.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +1 -0
- package/dist/web-appearance-tools.js +7 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance.d.ts +4 -10
- package/dist/web-appearance.js +4 -3
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-chat-history.d.ts +37 -0
- package/dist/web-chat-history.js +57 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-experimental-http.d.ts +2 -0
- package/dist/web-experimental-http.js +12 -0
- package/dist/web-experimental-http.js.map +1 -0
- package/dist/web-experimental.d.ts +11 -0
- package/dist/web-experimental.js +49 -0
- package/dist/web-experimental.js.map +1 -0
- package/dist/web-guest-access.d.ts +1 -0
- package/dist/web-guest-access.js +11 -3
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-icons.d.ts +33 -1
- package/dist/web-icons.js +83 -11
- package/dist/web-icons.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -0
- package/dist/web-mod-store.js +10 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-png.d.ts +29 -0
- package/dist/web-png.js +321 -0
- package/dist/web-png.js.map +1 -0
- package/dist/web-preview.d.ts +2 -0
- package/dist/web-preview.js +2 -0
- package/dist/web-preview.js.map +1 -1
- package/dist/web-reactions.d.ts +34 -0
- package/dist/web-reactions.js +47 -0
- package/dist/web-reactions.js.map +1 -0
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +5 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +1 -0
- package/dist/web-settings-tools.js +14 -10
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal.js +1 -1
- package/dist/web-theme-store.d.ts +2 -2
- package/dist/web-theme-store.js +6 -3
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
1
2
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
2
3
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
3
4
|
import { SettingsConfirmationStore } from "./settings-confirmation.js";
|
|
@@ -8,7 +9,8 @@ import { performance } from "node:perf_hooks";
|
|
|
8
9
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
9
10
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
10
11
|
import { createPublicWebGateway, PUBLIC_GATEWAY_RECEIVE_MS } from "./public-web-gateway.js";
|
|
11
|
-
import { handleWebIconRequest } from "./web-icons.js";
|
|
12
|
+
import { handleWebIconRequest, useAppIconStore } from "./web-icons.js";
|
|
13
|
+
import { AppIconStore } from "./web-app-icon.js";
|
|
12
14
|
import { trackWebServer } from "./web-server-drain.js";
|
|
13
15
|
import { renderPublicLinkProgress, ThrottledMessageEditor } from "./public-link-progress.js";
|
|
14
16
|
import { PublicWebLink, publicLinkSettings } from "./public-web-link.js";
|
|
@@ -75,10 +77,12 @@ import { routeToolCall } from "./channel/tool-router.js";
|
|
|
75
77
|
import { Scheduler } from "./scheduler/index.js";
|
|
76
78
|
import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
|
|
77
79
|
import { TaskBoardWaker } from "./task-board-waker.js";
|
|
80
|
+
import { TaskBoardError, wrapUpGap } from "./scheduler/task-board.js";
|
|
78
81
|
import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
|
|
79
82
|
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
80
83
|
import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
|
|
81
84
|
import { DailySummary } from "./daily-summary.js";
|
|
85
|
+
import { TaskBoardSummaryClock, buildSummary, isQuiet, renderSummary, summaryMarkFile, summaryTime } from "./task-board-summary.js";
|
|
82
86
|
import { WebhookEmitter } from "./webhook-emitter.js";
|
|
83
87
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
84
88
|
import { safeHandler } from "./safe-async.js";
|
|
@@ -143,6 +147,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
143
147
|
import { handleWebRequest, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
144
148
|
import { LogService } from "./log-service.js";
|
|
145
149
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
150
|
+
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
146
151
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
147
152
|
import { SeenUsers, resolveMentions } from "./web-mentions.js";
|
|
148
153
|
import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
|
|
@@ -244,13 +249,16 @@ import { decisionListAnswer } from "./decision-list.js";
|
|
|
244
249
|
import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
|
|
245
250
|
import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
|
|
246
251
|
import { createPreviewListener, normalizeOrigin, previewAvailability, previewSettings } from "./web-preview.js";
|
|
252
|
+
import { canvasWorkerEnabled, isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
|
|
253
|
+
import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
|
|
254
|
+
import { bundledSkillContents } from "./bundled-skills.js";
|
|
247
255
|
import { createEffectBootstrap } from "./web-effect-bootstrap.js";
|
|
248
256
|
import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
|
|
249
257
|
import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
|
|
250
258
|
import { SecretStore } from "./secret-store.js";
|
|
251
259
|
import { opaqueId, safeSecretError, SECRET_CHALLENGE_TTL_MS, } from "./connection-secrets.js";
|
|
252
260
|
import { verifyDiscordToken, verifyTelegramToken } from "./provider-probe.js";
|
|
253
|
-
import { PROVIDER_SECRET_SPECS, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
|
|
261
|
+
import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
|
|
254
262
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
255
263
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
256
264
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
@@ -260,6 +268,7 @@ import { mergeBackendOptions, profileKey, profileName, profileOf } from "./class
|
|
|
260
268
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
261
269
|
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
262
270
|
import { launchFullRestartHelper } from "./full-restart.js";
|
|
271
|
+
import { fullReloadExitCode } from "./service-owner.js";
|
|
263
272
|
import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
|
|
264
273
|
import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
|
|
265
274
|
import { StormWindow } from "./storm-window.js";
|
|
@@ -670,6 +679,8 @@ export class FleetManager {
|
|
|
670
679
|
costGuard = null;
|
|
671
680
|
statuslineWatcher;
|
|
672
681
|
dailySummary = null;
|
|
682
|
+
/** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
|
|
683
|
+
taskBoardSummary = null;
|
|
673
684
|
dailyTipScheduler = null;
|
|
674
685
|
/** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
|
|
675
686
|
taskBoardWaker = null;
|
|
@@ -846,6 +857,7 @@ export class FleetManager {
|
|
|
846
857
|
effectBootstrap = createEffectBootstrap();
|
|
847
858
|
/** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
|
|
848
859
|
get effectBoot() { return this.effectBootstrap.boot; }
|
|
860
|
+
canvasWorkerEnabled() { return canvasWorkerEnabled(this.fleetConfig?.web); }
|
|
849
861
|
healthServerDrain = null;
|
|
850
862
|
/** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
|
|
851
863
|
previewListener = null;
|
|
@@ -1092,6 +1104,7 @@ export class FleetManager {
|
|
|
1092
1104
|
/** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
|
|
1093
1105
|
get appearanceTools() {
|
|
1094
1106
|
return (this.appearanceToolsService ??= new AppearanceTools({
|
|
1107
|
+
canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
|
|
1095
1108
|
frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
|
|
1096
1109
|
theme: (name) => {
|
|
1097
1110
|
const r = this.themeStore.get(name);
|
|
@@ -1136,6 +1149,7 @@ export class FleetManager {
|
|
|
1136
1149
|
prefs: this.webPrefs,
|
|
1137
1150
|
ownerCss: this.ownerCss,
|
|
1138
1151
|
effects: this.effectLibrary,
|
|
1152
|
+
canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
|
|
1139
1153
|
context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
|
|
1140
1154
|
isKnownInstance: (name) => this.isKnownInstance(name),
|
|
1141
1155
|
prefsIdentity: OPERATOR,
|
|
@@ -1174,6 +1188,11 @@ export class FleetManager {
|
|
|
1174
1188
|
const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
|
|
1175
1189
|
return { ...(card.tagline ? { tagline: card.tagline.text } : {}), ...(card.bio ? { bio: card.bio.text } : {}) };
|
|
1176
1190
|
}
|
|
1191
|
+
appIconStore;
|
|
1192
|
+
/** #2308: the person's own app icon (Settings → Web appearance), under the state dir; none set: the bundled one. */
|
|
1193
|
+
get appIcon() {
|
|
1194
|
+
return (this.appIconStore ??= new AppIconStore(this.dataDir));
|
|
1195
|
+
}
|
|
1177
1196
|
ownerCssStore;
|
|
1178
1197
|
/** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
|
|
1179
1198
|
get ownerCss() {
|
|
@@ -1186,6 +1205,7 @@ export class FleetManager {
|
|
|
1186
1205
|
*/
|
|
1187
1206
|
get webSettings() {
|
|
1188
1207
|
return (this.webSettingsToolsService ??= new WebSettingsTools({
|
|
1208
|
+
canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
|
|
1189
1209
|
prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
|
|
1190
1210
|
instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
|
|
1191
1211
|
...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
|
|
@@ -1303,6 +1323,7 @@ export class FleetManager {
|
|
|
1303
1323
|
return null;
|
|
1304
1324
|
const record = auth.session, sessions = this.webSessions, exposure = gatewayRequestContext(req);
|
|
1305
1325
|
return { id: record.idHash, label: record.label, source: exposure ? "public_link" : "web_session",
|
|
1326
|
+
localAdmin: () => record.tier === "admin" && record.surface === "local" && !exposure,
|
|
1306
1327
|
current: () => current() && sessions === this.webSessions && !!this.webToken
|
|
1307
1328
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
1308
1329
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
@@ -2651,6 +2672,10 @@ export class FleetManager {
|
|
|
2651
2672
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
2652
2673
|
this.warnAboutRemovedLoginMode(loaded);
|
|
2653
2674
|
this.enforceGuestSwitch();
|
|
2675
|
+
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
2676
|
+
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
2677
|
+
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
2678
|
+
process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
|
|
2654
2679
|
return this.fleetConfig;
|
|
2655
2680
|
}
|
|
2656
2681
|
relayLoginModeWarned = false;
|
|
@@ -2830,8 +2855,17 @@ export class FleetManager {
|
|
|
2830
2855
|
this.logger.info(`Received SIGUSR1, initiating full restart (process reload)... ${describeSignalSource(this.dataDir, "SIGUSR1")}`);
|
|
2831
2856
|
this.gracefulShutdownForReload()
|
|
2832
2857
|
.then(() => {
|
|
2833
|
-
|
|
2834
|
-
|
|
2858
|
+
// #2356: under an AgEnD systemd unit a clean exit is a stop (Restart=on-failure); exit so the unit comes back.
|
|
2859
|
+
let ownCgroup = null;
|
|
2860
|
+
try {
|
|
2861
|
+
ownCgroup = readFileSync("/proc/self/cgroup", "utf-8");
|
|
2862
|
+
}
|
|
2863
|
+
catch { /* not Linux */ }
|
|
2864
|
+
const code = fullReloadExitCode(ownCgroup);
|
|
2865
|
+
this.logger.info(code === 0
|
|
2866
|
+
? "Full restart: shutdown complete, exiting for reload"
|
|
2867
|
+
: `Full restart: shutdown complete, exiting ${code} so the systemd unit restarts the fleet (#2356)`);
|
|
2868
|
+
process.exit(code);
|
|
2835
2869
|
})
|
|
2836
2870
|
.catch(err => {
|
|
2837
2871
|
this.logger.error({ err }, "Full restart: graceful shutdown failed");
|
|
@@ -2850,7 +2884,7 @@ export class FleetManager {
|
|
|
2850
2884
|
const chatId = String(this.getChannelConfig(adapterId)?.group_id ?? data.channelId);
|
|
2851
2885
|
this.beginUpdateProgress(adapter, chatId, data.channelId, messageId);
|
|
2852
2886
|
}
|
|
2853
|
-
const { spawn } = await import("
|
|
2887
|
+
const { spawn } = await import("./child-process.js");
|
|
2854
2888
|
// Plain `agend update`: the CLI picks the channel from the version it is about to replace (a beta install
|
|
2855
2889
|
// stays on beta). Deciding here, from the package.json next to THIS code, read a source checkout's 1.22.0 as
|
|
2856
2890
|
// "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
|
|
@@ -3792,12 +3826,61 @@ export class FleetManager {
|
|
|
3792
3826
|
* #1920 §5.2: a web message's channel copy was sent — it is now what the channel saw. Kept so (and on disk), and sent
|
|
3793
3827
|
* to the guests' streams only: an admin's page already has it, and nothing about it changed for them.
|
|
3794
3828
|
*/
|
|
3795
|
-
webMessageInChannel(instance, messageId) {
|
|
3829
|
+
webMessageInChannel(instance, messageId, copies = []) {
|
|
3830
|
+
// #2305: the copies' own message ids, so an instance's reaction to the web message is put on them too.
|
|
3831
|
+
if (copies.length)
|
|
3832
|
+
this.rememberWebEchoCopies(messageId, copies);
|
|
3796
3833
|
const m = this.webChatHistory.markChannel(instance, messageId);
|
|
3797
3834
|
if (m)
|
|
3798
3835
|
broadcastToGuests(this.sseClients, "message", m, (err) => this.logger.debug({ err }, "SSE client write failed; evicting"));
|
|
3799
3836
|
}
|
|
3800
|
-
|
|
3837
|
+
/** #2305: a web message's channel copies (topic, ClassicBot rooms), newest WEB_ECHO_COPIES_MAX, this process only. */
|
|
3838
|
+
webEchoCopies = new Map();
|
|
3839
|
+
rememberWebEchoCopies(messageId, copies) {
|
|
3840
|
+
const list = [...(this.webEchoCopies.get(messageId) ?? []), ...copies.filter(c => c && typeof c.adapter?.react === "function" && c.messageId)];
|
|
3841
|
+
this.webEchoCopies.delete(messageId);
|
|
3842
|
+
this.webEchoCopies.set(messageId, list);
|
|
3843
|
+
while (this.webEchoCopies.size > WEB_ECHO_COPIES_MAX)
|
|
3844
|
+
this.webEchoCopies.delete(this.webEchoCopies.keys().next().value);
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* #2305: an instance's reaction to a web chat message (`messageId`, the id it was handed): its minimal reply, as on
|
|
3848
|
+
* the platforms. Kept on the message's row (and on disk), shown live (`reactions` on the stream), and put on the
|
|
3849
|
+
* message's channel copies when this process sent them — in the background, each within WEB_MIRROR_TIMEOUT_MS, so a
|
|
3850
|
+
* slow or rate-limited platform never holds the tool's answer (best effort: a copy's failure or timeout is logged,
|
|
3851
|
+
* never the tool's). `mirrored`: how many copies took it, when that is known.
|
|
3852
|
+
* The tool's errors: an emoji outside the web set or one of the instance's delivery marks (web-reactions.ts), or a
|
|
3853
|
+
* message the chat does not have. An emoji the message already has is a success that changes nothing.
|
|
3854
|
+
*/
|
|
3855
|
+
async reactToWebMessage(instance, messageId, raw) {
|
|
3856
|
+
const problem = webReactionProblem(raw, this.statusEmojiAvoidList(instance));
|
|
3857
|
+
if (problem)
|
|
3858
|
+
return { ok: false, error: problem };
|
|
3859
|
+
const emoji = webReactionEmoji(raw);
|
|
3860
|
+
const r = this.webChatHistory.addReaction(instance, messageId, emoji);
|
|
3861
|
+
if (!r)
|
|
3862
|
+
return { ok: false, error: `react: ${messageId} is not a message in ${instance}'s web chat now (it left the history, or it is from before AgEnD restarted) — answer with reply instead` };
|
|
3863
|
+
if (!r.added)
|
|
3864
|
+
return { ok: true, mirrored: Promise.resolve(0) };
|
|
3865
|
+
this.emitSseEvent("reactions", { instance, boot: r.message.boot, id: r.message.id, reactions: r.message.reactions ?? [] });
|
|
3866
|
+
const mirrorOne = (copy) => {
|
|
3867
|
+
let timer;
|
|
3868
|
+
const timeout = new Promise((_, reject) => {
|
|
3869
|
+
timer = setTimeout(() => reject(new Error(`no answer in ${WEB_MIRROR_TIMEOUT_MS} ms`)), WEB_MIRROR_TIMEOUT_MS);
|
|
3870
|
+
timer.unref?.();
|
|
3871
|
+
});
|
|
3872
|
+
return Promise.race([Promise.resolve().then(() => copy.adapter.react(copy.chatId, copy.messageId, reactionForm(copy.adapter.type, emoji), copy.threadId)), timeout])
|
|
3873
|
+
.then(() => 1, (err) => {
|
|
3874
|
+
this.logger.warn({ err, instance, adapter: copy.adapter.type }, "Reaction on a web message's channel copy failed (shown on the web)");
|
|
3875
|
+
return 0;
|
|
3876
|
+
})
|
|
3877
|
+
.finally(() => clearTimeout(timer));
|
|
3878
|
+
};
|
|
3879
|
+
const mirrored = Promise.all((this.webEchoCopies.get(messageId) ?? []).map(mirrorOne)).then(n => n.reduce((a, b) => a + b, 0));
|
|
3880
|
+
return { ok: true, mirrored };
|
|
3881
|
+
}
|
|
3882
|
+
/** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
|
|
3883
|
+
async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK, copies) {
|
|
3801
3884
|
const guard = this.webChannelEchoGuards.get(instance);
|
|
3802
3885
|
const handled = new Set();
|
|
3803
3886
|
let posted = 0;
|
|
@@ -3827,7 +3910,9 @@ export class FleetManager {
|
|
|
3827
3910
|
try {
|
|
3828
3911
|
if (!adapter?.sendText)
|
|
3829
3912
|
continue;
|
|
3830
|
-
await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
|
|
3913
|
+
const sent = await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
|
|
3914
|
+
if (copies && sent && typeof sent.messageId === "string" && sent.messageId)
|
|
3915
|
+
copies.push({ adapter, chatId: entry.channelId, messageId: sent.messageId });
|
|
3831
3916
|
ClassicChannelManager.logMessage(instance, sender, text, new Date());
|
|
3832
3917
|
posted++;
|
|
3833
3918
|
}
|
|
@@ -4245,6 +4330,18 @@ export class FleetManager {
|
|
|
4245
4330
|
async dispatchSlashCore(slash, adapterId, adapter) {
|
|
4246
4331
|
// #1569: an alias (/web) is its command (/dashboard) from here on — the door, the table and the handler see one name.
|
|
4247
4332
|
const data = { ...slash, command: canonicalCommand(slash.command) };
|
|
4333
|
+
// #2359: /dashboard stays listed; prefix its reply with the deprecation notice, then run /web's handler.
|
|
4334
|
+
const noteDeprecatedDashboard = () => {
|
|
4335
|
+
if (slash.command !== "dashboard")
|
|
4336
|
+
return;
|
|
4337
|
+
const prefix = `${t("dashboard.deprecated")}\n\n`;
|
|
4338
|
+
const respond = data.respond;
|
|
4339
|
+
data.respond = async (text) => respond(prefix + text);
|
|
4340
|
+
if (data.respondButtons) {
|
|
4341
|
+
const respondButtons = data.respondButtons;
|
|
4342
|
+
data.respondButtons = async (text, choices) => respondButtons(prefix + text, choices);
|
|
4343
|
+
}
|
|
4344
|
+
};
|
|
4248
4345
|
const door = this.slashDoor(data, adapterId);
|
|
4249
4346
|
if ("refusal" in door) {
|
|
4250
4347
|
await data.respond(t(door.refusal)).catch(() => { });
|
|
@@ -4255,6 +4352,7 @@ export class FleetManager {
|
|
|
4255
4352
|
// — before the table, which keeps /web for fleet admins. Per account: it touches no instance binding.
|
|
4256
4353
|
// Decided synchronously, so an admin's /web still reaches the table with no await after the door (#1399).
|
|
4257
4354
|
if (data.command === "dashboard" && this.isGuestWebAsk(adapterId, data.userId)) {
|
|
4355
|
+
noteDeprecatedDashboard();
|
|
4258
4356
|
await this.guestWebCommand({ adapterId, userId: data.userId, username: data.username,
|
|
4259
4357
|
arg: String(data.options?.action ?? "").trim().toLowerCase(), replyHere: text => data.respond(text), replyPrivately: text => data.respond(text) });
|
|
4260
4358
|
return;
|
|
@@ -4283,6 +4381,7 @@ export class FleetManager {
|
|
|
4283
4381
|
await data.respond(reply).catch(() => { });
|
|
4284
4382
|
return;
|
|
4285
4383
|
}
|
|
4384
|
+
noteDeprecatedDashboard();
|
|
4286
4385
|
if (data.command === "start") {
|
|
4287
4386
|
await this.handleClassicStartSlash(data, adapterId);
|
|
4288
4387
|
}
|
|
@@ -5866,7 +5965,7 @@ export class FleetManager {
|
|
|
5866
5965
|
dataDir: this.dataDir, web: () => this.fleetConfig?.web,
|
|
5867
5966
|
permitted: owner => this.publicOwnerCurrent(owner),
|
|
5868
5967
|
reserve: id => this.getTunnelLane().reserve("dashboard", id),
|
|
5869
|
-
createGateway: (exposureId, current, open, failed) => createPublicWebGateway({ exposureId, isCurrent: current, isOpen: open, onError: failed,
|
|
5968
|
+
createGateway: (exposureId, current, open, failed, named) => createPublicWebGateway({ ...named, exposureId, isCurrent: current, isOpen: open, onError: failed,
|
|
5870
5969
|
dispatch: (req, res) => this.dispatchWebHttp(req, res, this.fleetConfig?.health_port ?? 19280) }),
|
|
5871
5970
|
revoke: id => {
|
|
5872
5971
|
this.webLoginCodes?.revokeAudience(id);
|
|
@@ -7099,6 +7198,17 @@ export class FleetManager {
|
|
|
7099
7198
|
return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
|
|
7100
7199
|
});
|
|
7101
7200
|
this.dailySummary.start();
|
|
7201
|
+
// #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
|
|
7202
|
+
// in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
|
|
7203
|
+
this.taskBoardSummary = new TaskBoardSummaryClock({
|
|
7204
|
+
at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
|
|
7205
|
+
zone: () => this.usageZone(),
|
|
7206
|
+
now: () => new Date(),
|
|
7207
|
+
mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
|
|
7208
|
+
send: (since, until) => this.postTaskBoardSummary(since, until),
|
|
7209
|
+
failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
|
|
7210
|
+
});
|
|
7211
|
+
this.taskBoardSummary.start();
|
|
7102
7212
|
// Rotate classic channel chat logs daily (piggyback on daily summary timer)
|
|
7103
7213
|
this.classicChannels?.rotateLogs();
|
|
7104
7214
|
this.rotateInboxes();
|
|
@@ -7493,7 +7603,8 @@ export class FleetManager {
|
|
|
7493
7603
|
}, 60 * 60 * 1000);
|
|
7494
7604
|
const onRestart = () => this.onGracefulRestartSignal(() => process.once("SIGUSR2", onRestart));
|
|
7495
7605
|
process.once("SIGUSR2", onRestart);
|
|
7496
|
-
// SIGUSR1: full process reload (graceful stop → exit
|
|
7606
|
+
// SIGUSR1: full process reload (graceful stop → exit). Nothing in this AgEnD sends it any more (#2356); an older
|
|
7607
|
+
// CLI or a hand-sent signal still may, and the exit code then lets the owning manager start the next fleet.
|
|
7497
7608
|
process.once("SIGUSR1", () => this.onFullRestartSignal());
|
|
7498
7609
|
// A SIGHUP may arrive after the PID/general is available but before the
|
|
7499
7610
|
// rest of startup finishes. Replay one coalesced reload only after all
|
|
@@ -8384,7 +8495,7 @@ export class FleetManager {
|
|
|
8384
8495
|
// means we cannot verify the pane, which is treated as dead (the same
|
|
8385
8496
|
// conclusion the old code reached only by throwing).
|
|
8386
8497
|
try {
|
|
8387
|
-
const { execFile } = await import("
|
|
8498
|
+
const { execFile } = await import("./child-process.js");
|
|
8388
8499
|
const { promisify } = await import("node:util");
|
|
8389
8500
|
const { getTmuxSocketName } = await import("./paths.js");
|
|
8390
8501
|
// Honour socket isolation: without -L this queried the user's default
|
|
@@ -8934,6 +9045,22 @@ export class FleetManager {
|
|
|
8934
9045
|
this.logger.debug({ adapterId: msg.adapterId, threadId: threadId ?? null, messageId: msg.messageId, reason: drop }, "Inbound message dropped before the dedup claim — key not consumed");
|
|
8935
9046
|
return;
|
|
8936
9047
|
}
|
|
9048
|
+
// #2246 step 3 (#2341 review r2): a reply to a Needs you Send back / Answer prompt is the card's reason or answer, not
|
|
9049
|
+
// input for an instance — judged by the PROMPT's world (its admins, its instance, its room as it is now; the hub's
|
|
9050
|
+
// handleReply), before the room's generic access below: in a shared group the room may be another bot's, whose
|
|
9051
|
+
// policy has no say over that world's prompt. Only that exact reply, by the admin who clicked, while the prompt
|
|
9052
|
+
// stands. Taken once: another bot's copy of a taken reply is dropped here. All else flows on.
|
|
9053
|
+
if (!msg.isBotMessage && msg.replyTo && this.needsYou) {
|
|
9054
|
+
if (this.needsYou.wasTakenReply(msg.chatId, msg.messageId)) {
|
|
9055
|
+
this.logger.debug({ adapterId: msg.adapterId, messageId: msg.messageId }, "Another bot's copy of a Needs you answer already taken — dropped");
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
if (this.needsYou.handleReply({
|
|
9059
|
+
adapterId: msg.adapterId, chatId: msg.chatId, ...(threadId !== undefined ? { threadId } : {}), replyTo: msg.replyTo,
|
|
9060
|
+
messageId: msg.messageId, userId: msg.userId, ...(msg.username ? { username: msg.username } : {}), text: msg.text ?? "",
|
|
9061
|
+
}))
|
|
9062
|
+
return;
|
|
9063
|
+
}
|
|
8937
9064
|
// Access control — classic channels are open to all, others require an allowed
|
|
8938
9065
|
// user. This runs before the dedup claim, and is judged by the adapter that
|
|
8939
9066
|
// owns the topic rather than the one that happened to receive the message:
|
|
@@ -9044,6 +9171,10 @@ export class FleetManager {
|
|
|
9044
9171
|
// #1920 step 4b-3: /web typed in a ClassicBot room (a group: `/web@ThisBot`) by someone who is not a fleet admin,
|
|
9045
9172
|
// while guest sign-in is on: their guest code, privately — as anywhere else. An admin's goes on as before.
|
|
9046
9173
|
if (/^\/(?:web|dashboard)(?:\s|$)/i.test(text) && msgAdapter && this.isGuestWebAsk(msg.adapterId, msg.userId)) {
|
|
9174
|
+
// #2359: this door must not skip the deprecation notice TopicCommands shows on /dashboard.
|
|
9175
|
+
if (/^\/dashboard(?:\s|$)/i.test(text)) {
|
|
9176
|
+
await msgAdapter.sendText(chatId, t("dashboard.deprecated")).catch(() => { });
|
|
9177
|
+
}
|
|
9047
9178
|
await this.guestWebCommand({ adapterId: msg.adapterId, userId: msg.userId, username: msg.username,
|
|
9048
9179
|
arg: text.replace(/^\/(?:web|dashboard)/i, "").trim().toLowerCase(), replyHere: body => msgAdapter.sendText(chatId, body) });
|
|
9049
9180
|
return;
|
|
@@ -9594,6 +9725,8 @@ export class FleetManager {
|
|
|
9594
9725
|
}
|
|
9595
9726
|
}
|
|
9596
9727
|
checkToolPermission(sink, instanceName, tool) {
|
|
9728
|
+
if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
|
|
9729
|
+
return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
|
|
9597
9730
|
const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
|
|
9598
9731
|
const allowed = mayUseTool(profile, tool);
|
|
9599
9732
|
if (!allowed) {
|
|
@@ -9649,6 +9782,17 @@ export class FleetManager {
|
|
|
9649
9782
|
});
|
|
9650
9783
|
return;
|
|
9651
9784
|
}
|
|
9785
|
+
// #2305: a reaction to a web chat message is the web chat's own (any fleet, web-only included): never an adapter's.
|
|
9786
|
+
if (tool === "react" && typeof args.message_id === "string" && isWebMessageId(args.message_id)) {
|
|
9787
|
+
// The socket's own instance, never a session name the caller wrote itself (as checkToolPermission: it was not
|
|
9788
|
+
// identified) — a channel tool's call from a daemon carries none anyway (#2310 review).
|
|
9789
|
+
const r = await this.reactToWebMessage(instanceName, args.message_id, args.emoji);
|
|
9790
|
+
if (r.ok)
|
|
9791
|
+
respond("ok");
|
|
9792
|
+
else
|
|
9793
|
+
respond(null, r.error);
|
|
9794
|
+
return;
|
|
9795
|
+
}
|
|
9652
9796
|
// A fleet with no chat platform at all is driven from the web dashboard alone: a reply has nowhere
|
|
9653
9797
|
// else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
|
|
9654
9798
|
const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
|
|
@@ -10267,13 +10411,14 @@ export class FleetManager {
|
|
|
10267
10411
|
if (!ipc || !fleetRequestId)
|
|
10268
10412
|
return;
|
|
10269
10413
|
const type = String(msg.type);
|
|
10270
|
-
const responseType = type.
|
|
10271
|
-
: type.startsWith("
|
|
10272
|
-
: type
|
|
10273
|
-
: type === "
|
|
10274
|
-
: type === "
|
|
10275
|
-
|
|
10276
|
-
|
|
10414
|
+
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
|
|
10415
|
+
: type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
|
|
10416
|
+
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
10417
|
+
: type === "fleet_task" ? "fleet_task_response"
|
|
10418
|
+
: type === "fleet_set_display_name" ? "fleet_display_name_response"
|
|
10419
|
+
: type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
|
|
10420
|
+
|| type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
|
|
10421
|
+
: "fleet_description_response";
|
|
10277
10422
|
ipc.send({ type: responseType, fleetRequestId, error: message });
|
|
10278
10423
|
}
|
|
10279
10424
|
dispatchTypedIpc(name, msg) {
|
|
@@ -10766,6 +10911,8 @@ export class FleetManager {
|
|
|
10766
10911
|
}
|
|
10767
10912
|
/** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
|
|
10768
10913
|
async handleWebThemeHttp(instance, tool, args) {
|
|
10914
|
+
if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
|
|
10915
|
+
return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
|
|
10769
10916
|
if (WEB_EFFECT_TOOLS.has(tool)) {
|
|
10770
10917
|
if (!this.isKnownInstance(instance))
|
|
10771
10918
|
return { error: "unknown_instance" };
|
|
@@ -12658,7 +12805,7 @@ export class FleetManager {
|
|
|
12658
12805
|
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
12659
12806
|
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
12660
12807
|
try {
|
|
12661
|
-
const { execFile } = await import("
|
|
12808
|
+
const { execFile } = await import("./child-process.js");
|
|
12662
12809
|
const { promisify } = await import("node:util");
|
|
12663
12810
|
// execFile with an argv array — no shell, so the backend name cannot be
|
|
12664
12811
|
// interpreted as a command even if config is malformed.
|
|
@@ -12884,6 +13031,51 @@ export class FleetManager {
|
|
|
12884
13031
|
}
|
|
12885
13032
|
return { chatId, opts: {} };
|
|
12886
13033
|
}
|
|
13034
|
+
/**
|
|
13035
|
+
* #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
|
|
13036
|
+
* gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
|
|
13037
|
+
* one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
|
|
13038
|
+
* nothing. A web-only fleet: everything, in the web chat.
|
|
13039
|
+
*/
|
|
13040
|
+
postTaskBoardSummary(since, until) {
|
|
13041
|
+
const board = this.scheduler?.db.board;
|
|
13042
|
+
if (!board)
|
|
13043
|
+
return;
|
|
13044
|
+
const read = board.summaryRead(since.toISOString(), until.toISOString());
|
|
13045
|
+
const projects = board.listProjects(true);
|
|
13046
|
+
const stats = board.projectStats();
|
|
13047
|
+
const names = new Map(projects.map((p) => [p.id, p.name]));
|
|
13048
|
+
const configured = this.getPrimaryAdapterId();
|
|
13049
|
+
const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
|
|
13050
|
+
const worldOf = (assignee) => {
|
|
13051
|
+
const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
|
|
13052
|
+
return owner !== undefined && this.worlds.has(owner) ? owner : primary;
|
|
13053
|
+
};
|
|
13054
|
+
const zone = this.usageZone();
|
|
13055
|
+
for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
|
|
13056
|
+
const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
|
|
13057
|
+
if (isQuiet(summary))
|
|
13058
|
+
continue;
|
|
13059
|
+
const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
|
|
13060
|
+
const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
|
|
13061
|
+
if (world !== undefined) {
|
|
13062
|
+
const adapter = this.worlds.get(world)?.adapter;
|
|
13063
|
+
const target = this.fleetNoticeTarget(world);
|
|
13064
|
+
if (adapter && target) {
|
|
13065
|
+
adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
|
|
13066
|
+
}
|
|
13067
|
+
else {
|
|
13068
|
+
this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
|
|
13069
|
+
}
|
|
13070
|
+
}
|
|
13071
|
+
for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
13072
|
+
if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
|
|
13073
|
+
continue;
|
|
13074
|
+
// #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
|
|
13075
|
+
this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
|
|
13076
|
+
}
|
|
13077
|
+
}
|
|
13078
|
+
}
|
|
12887
13079
|
/**
|
|
12888
13080
|
* Post the daily summary where fleet-wide notices go.
|
|
12889
13081
|
*
|
|
@@ -13592,6 +13784,8 @@ export class FleetManager {
|
|
|
13592
13784
|
return String(topic);
|
|
13593
13785
|
return this.classicChannels?.getChannelIdByInstance(instance) ?? undefined;
|
|
13594
13786
|
},
|
|
13787
|
+
laneTarget: instance => this.instanceChannelTarget(instance),
|
|
13788
|
+
taskRoute: code => this.needsYouRoute(code),
|
|
13595
13789
|
isFleetAdmin: (userId, world) => this.isFleetAdmin(userId, world),
|
|
13596
13790
|
fleetAdmins: world => (this.getChannelConfig(world)?.access?.allowed_users ?? []).map(String),
|
|
13597
13791
|
emitSse: (event, data) => this.emitSseEvent(event, data),
|
|
@@ -13600,6 +13794,8 @@ export class FleetManager {
|
|
|
13600
13794
|
throw new Error("the delivery outbox is not open");
|
|
13601
13795
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
13602
13796
|
},
|
|
13797
|
+
tasks: () => this.needsYouTasks(),
|
|
13798
|
+
taskAction: action => this.taskActionFromChat(action),
|
|
13603
13799
|
settings: () => ({
|
|
13604
13800
|
liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
|
|
13605
13801
|
dm: this.fleetConfig?.needs_you?.dm === true,
|
|
@@ -13610,6 +13806,98 @@ export class FleetManager {
|
|
|
13610
13806
|
});
|
|
13611
13807
|
this.needsYou.start();
|
|
13612
13808
|
}
|
|
13809
|
+
/**
|
|
13810
|
+
* #2352: where an instance's own notices go — its topic in its world's group, or its ClassicBot room — as a Needs you
|
|
13811
|
+
* lane's place; null when it has neither (or its world has no group).
|
|
13812
|
+
*/
|
|
13813
|
+
instanceChannelTarget(instance) {
|
|
13814
|
+
const cfg = this.fleetConfig?.instances && Object.hasOwn(this.fleetConfig.instances, instance) ? this.fleetConfig.instances[instance] : undefined;
|
|
13815
|
+
if (!cfg)
|
|
13816
|
+
return null;
|
|
13817
|
+
const groupId = this.getChannelConfig(this.getInstanceAdapterId(instance))?.group_id;
|
|
13818
|
+
if (cfg.topic_id != null && groupId != null)
|
|
13819
|
+
return { chatId: String(groupId), threadId: String(cfg.topic_id) };
|
|
13820
|
+
const classic = this.classicChannels?.getChannelIdByInstance(instance);
|
|
13821
|
+
return classic ? { chatId: classic } : null;
|
|
13822
|
+
}
|
|
13823
|
+
/**
|
|
13824
|
+
* #2352: the instance whose own channel lists a waiting card — the instance that handed it out, else its project's
|
|
13825
|
+
* lead — when that channel can be used now (a fleet instance of a live world, with a topic or room). undefined: its
|
|
13826
|
+
* assignee's world's General, as before.
|
|
13827
|
+
*/
|
|
13828
|
+
needsYouLane(card, leads) {
|
|
13829
|
+
const usable = (name) => {
|
|
13830
|
+
if (!name || !this.fleetConfig?.instances || !Object.hasOwn(this.fleetConfig.instances, name))
|
|
13831
|
+
return false;
|
|
13832
|
+
const world = this.getInstanceAdapterId(name);
|
|
13833
|
+
return world !== undefined && this.worlds.has(world) && this.instanceChannelTarget(name) !== null;
|
|
13834
|
+
};
|
|
13835
|
+
const dispatcher = card.dispatcher?.kind === "instance" && card.dispatcher.name !== card.assignee ? card.dispatcher.name : null;
|
|
13836
|
+
if (dispatcher)
|
|
13837
|
+
return usable(dispatcher) ? dispatcher : undefined;
|
|
13838
|
+
const lead = card.projectId != null ? leads.get(card.projectId) : null;
|
|
13839
|
+
return usable(lead) ? lead : undefined;
|
|
13840
|
+
}
|
|
13841
|
+
/**
|
|
13842
|
+
* #2355 review: where a card's Needs you buttons belong now — read from the board and the fleet at the moment of a click
|
|
13843
|
+
* or a reply, never from the last render. null: it no longer waits for the person.
|
|
13844
|
+
*/
|
|
13845
|
+
needsYouRoute(code) {
|
|
13846
|
+
const board = this.scheduler?.db.board;
|
|
13847
|
+
const card = board?.findCard(code);
|
|
13848
|
+
if (!board || !card || !(card.status === "blocked" || (card.status === "review" && card.needsReview)))
|
|
13849
|
+
return null;
|
|
13850
|
+
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13851
|
+
const lane = this.needsYouLane(card, leads);
|
|
13852
|
+
return lane ? { lane } : {};
|
|
13853
|
+
}
|
|
13854
|
+
/**
|
|
13855
|
+
* #2246 step 3: the task board's cards that wait for the person, for Needs you — under each card's assignee, listed in
|
|
13856
|
+
* its lane's channel (#2352: the instance that handed it out, else the project's lead), else in General.
|
|
13857
|
+
*/
|
|
13858
|
+
needsYouTasks() {
|
|
13859
|
+
const board = this.scheduler?.db.board;
|
|
13860
|
+
if (!board)
|
|
13861
|
+
return [];
|
|
13862
|
+
const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
13863
|
+
return board.waitingForPerson().map(({ card, since }) => {
|
|
13864
|
+
const gap = card.status === "review" ? wrapUpGap(card) : null;
|
|
13865
|
+
const lane = this.needsYouLane(card, leads);
|
|
13866
|
+
return {
|
|
13867
|
+
uuid: card.uuid, code: card.code, title: card.title, status: card.status, version: card.version,
|
|
13868
|
+
assignee: card.assignee, since: Date.parse(since) || 0, ...(gap ? { gap } : {}), ...(lane ? { lane } : {}),
|
|
13869
|
+
note: card.status === "blocked" ? card.blockedReason : gap ? t("needs.task_gap", [
|
|
13870
|
+
gap.unchecked ? t("needs.task_gap_unchecked", gap.unchecked) : "", gap.noResult ? t("needs.task_gap_no_result") : "",
|
|
13871
|
+
].filter(Boolean).join(t("needs.task_gap_join"))) : "",
|
|
13872
|
+
};
|
|
13873
|
+
});
|
|
13874
|
+
}
|
|
13875
|
+
/**
|
|
13876
|
+
* #2246 step 3: the person's move on a card from a chat's Needs you list — a fleet admin of that world (checked by the
|
|
13877
|
+
* hub), acting as the person, through the board's rules, at the version the button showed.
|
|
13878
|
+
*/
|
|
13879
|
+
taskActionFromChat(action) {
|
|
13880
|
+
const board = this.scheduler?.db.board;
|
|
13881
|
+
if (!board)
|
|
13882
|
+
return { ok: false, message: t("needs.task_board_closed") };
|
|
13883
|
+
const person = { kind: "person", name: action.by.username || `${action.by.platform}:${action.by.userId}` };
|
|
13884
|
+
try {
|
|
13885
|
+
if (action.verb === "confirm")
|
|
13886
|
+
board.moveCard(person, action.code, "done", { version: action.version });
|
|
13887
|
+
else if (action.verb === "back")
|
|
13888
|
+
board.moveCard(person, action.code, "claimed", { version: action.version, reason: action.text });
|
|
13889
|
+
else
|
|
13890
|
+
board.moveCard(person, action.code, "claimed", { version: action.version, comment: action.text });
|
|
13891
|
+
const done = { confirm: "needs.task_confirmed", back: "needs.task_sent_back", answer: "needs.task_answered" }[action.verb];
|
|
13892
|
+
this.logger.info({ code: action.code, verb: action.verb, by: `${action.by.platform}:${action.by.userId}` }, "Task board: the person moved a card from the chat");
|
|
13893
|
+
return { ok: true, message: t(done, action.code) };
|
|
13894
|
+
}
|
|
13895
|
+
catch (err) {
|
|
13896
|
+
if (err instanceof TaskBoardError && err.code === "conflict")
|
|
13897
|
+
return { ok: false, message: t("needs.task_changed", action.code) };
|
|
13898
|
+
return { ok: false, message: t("needs.task_refused", action.code, err.message) };
|
|
13899
|
+
}
|
|
13900
|
+
}
|
|
13613
13901
|
/** The prompts open on the dashboard right now (a page that loads after one was posted asks for them). */
|
|
13614
13902
|
listWebPrompts() {
|
|
13615
13903
|
const open = [];
|
|
@@ -16268,6 +16556,7 @@ export class FleetManager {
|
|
|
16268
16556
|
channel: m.channel,
|
|
16269
16557
|
from: m.from,
|
|
16270
16558
|
replyTo: m.replyTo,
|
|
16559
|
+
cards: m.cards,
|
|
16271
16560
|
});
|
|
16272
16561
|
broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
|
|
16273
16562
|
this.searchIndex?.record(chatDoc(recorded));
|
|
@@ -16299,11 +16588,25 @@ export class FleetManager {
|
|
|
16299
16588
|
isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
|
|
16300
16589
|
holds: (wake) => db.board.wakeHolds(wake),
|
|
16301
16590
|
card: (uuid) => db.board.findCard(uuid),
|
|
16591
|
+
// #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
|
|
16592
|
+
buttonsHere: (instance, uuid) => {
|
|
16593
|
+
// #2355 review: no live message, no buttons — the old words then.
|
|
16594
|
+
if (this.fleetConfig?.needs_you?.live_message === false)
|
|
16595
|
+
return false;
|
|
16596
|
+
const card = db.board.findCard(uuid);
|
|
16597
|
+
if (!card)
|
|
16598
|
+
return false;
|
|
16599
|
+
const leads = new Map(db.board.listProjects(true).map((p) => [p.id, p.leadInstance]));
|
|
16600
|
+
return this.needsYouLane(card, leads) === instance;
|
|
16601
|
+
},
|
|
16302
16602
|
log: (level, message, extra) => this.logger[level](extra ?? {}, message),
|
|
16303
16603
|
});
|
|
16304
16604
|
db.board.onChange(({ rev, ids, projects, wakes }) => {
|
|
16305
16605
|
this.emitSseEvent("tasks", { rev, ids, projects });
|
|
16306
16606
|
waker.note(wakes);
|
|
16607
|
+
// step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
|
|
16608
|
+
if (ids.length || projects)
|
|
16609
|
+
this.needsYou?.poke();
|
|
16307
16610
|
});
|
|
16308
16611
|
}
|
|
16309
16612
|
/** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
|
|
@@ -16517,6 +16820,27 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16517
16820
|
this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
|
|
16518
16821
|
this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
|
|
16519
16822
|
}
|
|
16823
|
+
/** Update only AgEnD-managed skill copies; no launch or restart is needed for an experimental switch. */
|
|
16824
|
+
canvasWorkerSettingChanged() {
|
|
16825
|
+
this.refreshExperimentalSkills();
|
|
16826
|
+
this.emitSseEvent("status", this.getUiStatus());
|
|
16827
|
+
}
|
|
16828
|
+
refreshExperimentalSkills() {
|
|
16829
|
+
for (const [name, daemon] of this.daemons) {
|
|
16830
|
+
try {
|
|
16831
|
+
const cfg = daemon.getConfigSnapshot();
|
|
16832
|
+
const backend = cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
16833
|
+
const workDir = this.resolveKnowledgeWorkDir(cfg.working_directory, backend, name);
|
|
16834
|
+
const classic = this.classicChannels?.getAll().some(ch => ch.instanceName === name) === true;
|
|
16835
|
+
if (classic && !insideManagedWorkspaces(workDir))
|
|
16836
|
+
continue;
|
|
16837
|
+
this.syncRoleSkills(workDir, backend, classic ? "classic" : cfg.general_topic ? "general" : "worker");
|
|
16838
|
+
}
|
|
16839
|
+
catch (err) {
|
|
16840
|
+
this.logger.warn({ name, err }, "Could not refresh experimental skill visibility");
|
|
16841
|
+
}
|
|
16842
|
+
}
|
|
16843
|
+
}
|
|
16520
16844
|
/** Publish only the bundled skills eligible for an instance role. */
|
|
16521
16845
|
syncRoleSkills(workDir, backend, role, knowledgeDir) {
|
|
16522
16846
|
const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
|
|
@@ -16681,7 +17005,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16681
17005
|
continue;
|
|
16682
17006
|
}
|
|
16683
17007
|
managed.push(name);
|
|
16684
|
-
const newContent =
|
|
17008
|
+
const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
|
|
16685
17009
|
if (refuse(dest))
|
|
16686
17010
|
continue;
|
|
16687
17011
|
try {
|
|
@@ -19204,6 +19528,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19204
19528
|
this.clearStatuslineWatchers();
|
|
19205
19529
|
this.costGuard?.stop();
|
|
19206
19530
|
this.dailySummary?.stop();
|
|
19531
|
+
this.taskBoardSummary?.stop();
|
|
19207
19532
|
this.dailyTipScheduler?.stop();
|
|
19208
19533
|
this.dailyTipScheduler = null;
|
|
19209
19534
|
if (this.updateCheckTimer) {
|
|
@@ -19285,6 +19610,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19285
19610
|
await this.retirePendingNoncePrompts();
|
|
19286
19611
|
this.topicArchiver.stop();
|
|
19287
19612
|
this.taskBoardWaker?.stop();
|
|
19613
|
+
this.appIconStore?.close();
|
|
19288
19614
|
this.scheduler?.shutdown();
|
|
19289
19615
|
// What it was sent is written first (bounded: search-index.ts close()).
|
|
19290
19616
|
const search = this.searchIndex;
|
|
@@ -19539,6 +19865,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19539
19865
|
// mark every row done and tell the user "changes applied".
|
|
19540
19866
|
return { rejected: why };
|
|
19541
19867
|
}
|
|
19868
|
+
if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
|
|
19869
|
+
this.canvasWorkerSettingChanged();
|
|
19870
|
+
}
|
|
19542
19871
|
// Classic behavior settings share the fleet defaults but are not entries
|
|
19543
19872
|
// in fleet.yaml. Snapshot the old effective chain before reloading the
|
|
19544
19873
|
// Classic file so SIGHUP can hot-apply either source without waiting for
|
|
@@ -19788,6 +20117,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19788
20117
|
return false;
|
|
19789
20118
|
}
|
|
19790
20119
|
/** Non-null when startup found the running config and fleet.yaml disagreeing. */
|
|
20120
|
+
/**
|
|
20121
|
+
* #2346 (settings schema): whether a connection added now would start at once — what canHotAddConnections would decide
|
|
20122
|
+
* for the current config with one more connection appended: another one is running, and nothing else this process took
|
|
20123
|
+
* at startup has changed since (a pending locale or access change, say, needs AgEnD restarted anyway).
|
|
20124
|
+
*/
|
|
20125
|
+
connectionsStartLive() {
|
|
20126
|
+
if (!this.fleetConfig)
|
|
20127
|
+
return false;
|
|
20128
|
+
const current = this.fleetConfig.channels ?? (this.fleetConfig.channel ? [this.fleetConfig.channel] : []);
|
|
20129
|
+
const probe = { id: "\u0000probe", type: "telegram", mode: "topic", bot_token_env: "\u0000PROBE" };
|
|
20130
|
+
const next = { ...this.fleetConfig, channels: [...current, probe] };
|
|
20131
|
+
delete next.channel;
|
|
20132
|
+
return this.canHotAddConnections(next);
|
|
20133
|
+
}
|
|
19791
20134
|
fleetSignatureMismatchKeys() {
|
|
19792
20135
|
return this.fleetSignatureMismatch;
|
|
19793
20136
|
}
|
|
@@ -20006,12 +20349,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20006
20349
|
return this.fleetConfig?.web?.provider_secrets === true;
|
|
20007
20350
|
}
|
|
20008
20351
|
listProviderSecrets() {
|
|
20009
|
-
return PROVIDER_SECRET_SPECS.map(spec => ({
|
|
20352
|
+
return [...PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC].map(spec => ({
|
|
20010
20353
|
id: spec.id,
|
|
20011
20354
|
display_name: spec.displayName,
|
|
20012
20355
|
kind: spec.kind,
|
|
20013
20356
|
token_present: !!process.env[spec.envKey],
|
|
20014
|
-
verifier: spec.verifier ? "available" : "unsupported",
|
|
20357
|
+
verifier: spec.kind === "tunnel_token" ? "local_format" : spec.verifier ? "available" : "unsupported",
|
|
20015
20358
|
activation: spec.activation,
|
|
20016
20359
|
stale_consumers: this.providerSecretStaleConsumers(spec.envKey),
|
|
20017
20360
|
}));
|
|
@@ -20021,7 +20364,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20021
20364
|
// a child process's private environment safely, so report the conservative
|
|
20022
20365
|
// set of already-running children; the UI can then say "restart these"
|
|
20023
20366
|
// rather than claiming an existing process reloaded.
|
|
20024
|
-
if (envKey === "GROQ_API_KEY")
|
|
20367
|
+
if (envKey === "GROQ_API_KEY" || envKey === PUBLIC_TUNNEL_TOKEN_ENV)
|
|
20025
20368
|
return [];
|
|
20026
20369
|
return [...this.children.keys()].sort();
|
|
20027
20370
|
}
|
|
@@ -20039,10 +20382,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20039
20382
|
if (!spec || !this.providerSecretEnvAllowed(spec.envKey)) {
|
|
20040
20383
|
return { ok: false, status: "invalid", error: "provider secret is not configured" };
|
|
20041
20384
|
}
|
|
20042
|
-
|
|
20385
|
+
const tunnel = spec.kind === "tunnel_token";
|
|
20386
|
+
if (!tunnel && !spec.verifier)
|
|
20043
20387
|
return { ok: false, status: "unsupported_verifier", error: "this provider has no supported verifier" };
|
|
20044
|
-
|
|
20045
|
-
|
|
20388
|
+
const secret = tunnel && !input.clear ? normalizePublicTunnelToken(input.secret) : input.secret;
|
|
20389
|
+
if (input.clear && (!tunnel || input.secret !== "")
|
|
20390
|
+
|| !input.clear && (!secret || secret.length > 4096 || /[\r\n\0]/.test(secret) || /[^\x20-\x7e]/.test(secret))) {
|
|
20391
|
+
return { ok: false, status: "invalid", error: tunnel ? PUBLIC_TUNNEL_TOKEN_HINT : "secret is invalid" };
|
|
20046
20392
|
}
|
|
20047
20393
|
const challengeKey = `${input.sessionBinding}:api_key:${spec.id}:${spec.envKey}:${input.idempotencyKey}`;
|
|
20048
20394
|
const existingId = this.providerSecretChallengesByKey.get(challengeKey);
|
|
@@ -20052,7 +20398,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20052
20398
|
}
|
|
20053
20399
|
if (existingId)
|
|
20054
20400
|
this.providerSecretChallengesByKey.delete(challengeKey);
|
|
20055
|
-
const result = await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
|
|
20401
|
+
const result = tunnel ? { ok: true } : await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
|
|
20056
20402
|
if (!result.ok) {
|
|
20057
20403
|
// Do not log provider detail: the HTTP verifier already redacted it and
|
|
20058
20404
|
// this endpoint has no need to disclose whether a key was close to valid.
|
|
@@ -20070,7 +20416,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20070
20416
|
operation: "provider-secret.apply",
|
|
20071
20417
|
idempotencyKey: input.idempotencyKey,
|
|
20072
20418
|
expiresAt, deadline: performance.now() + SECRET_CHALLENGE_TTL_MS,
|
|
20073
|
-
secret:
|
|
20419
|
+
secret: secret,
|
|
20074
20420
|
};
|
|
20075
20421
|
this.providerSecretChallenges.set(challenge.id, challenge);
|
|
20076
20422
|
this.providerSecretChallengesByKey.set(challengeKey, challenge.id);
|
|
@@ -20140,6 +20486,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20140
20486
|
async runProviderSecretApply(job, secret, execution) {
|
|
20141
20487
|
const spec = providerSecretSpec(job.specId);
|
|
20142
20488
|
const allowed = new Set([
|
|
20489
|
+
PUBLIC_TUNNEL_TOKEN_ENV,
|
|
20143
20490
|
...PROVIDER_SECRET_SPECS.map(item => item.envKey),
|
|
20144
20491
|
...(this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).map(channel => channel.bot_token_env),
|
|
20145
20492
|
]);
|
|
@@ -20153,7 +20500,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20153
20500
|
// Construct inside the transaction: symlink/permission refusal must
|
|
20154
20501
|
// settle the job as a safe failure, not escape the queued microtask.
|
|
20155
20502
|
store = new SecretStore(join(this.dataDir, ".env"), allowed, { owner: execution?.owner });
|
|
20156
|
-
const write = () => {
|
|
20503
|
+
const write = () => {
|
|
20504
|
+
before = spec.kind === "tunnel_token" && !secret ? store.clear(job.envKey) : store.write(job.envKey, secret);
|
|
20505
|
+
wrote = true;
|
|
20506
|
+
if (secret)
|
|
20507
|
+
process.env[job.envKey] = secret;
|
|
20508
|
+
else
|
|
20509
|
+
delete process.env[job.envKey];
|
|
20510
|
+
};
|
|
20157
20511
|
if (execution)
|
|
20158
20512
|
execution.mutate(write);
|
|
20159
20513
|
else
|
|
@@ -20175,12 +20529,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20175
20529
|
job.finishedAt = Date.now();
|
|
20176
20530
|
}
|
|
20177
20531
|
catch (err) {
|
|
20178
|
-
const safe = safeSecretError(err, secret);
|
|
20532
|
+
const safe = spec?.kind === "tunnel_token" ? "connector token change failed" : safeSecretError(err, secret);
|
|
20179
20533
|
this.logger.warn({ specId: job.specId, reason: safe }, "Provider API-key apply failed");
|
|
20180
20534
|
try {
|
|
20181
20535
|
// Memory and disk have separate receipts: a host-edited file must
|
|
20182
20536
|
// remain, but cannot strand our owned process value or hook snapshot.
|
|
20183
|
-
if (wrote && process.env[job.envKey] === secret) {
|
|
20537
|
+
if (wrote && (process.env[job.envKey] ?? "") === secret) {
|
|
20184
20538
|
if (previousProcessValue === undefined)
|
|
20185
20539
|
delete process.env[job.envKey];
|
|
20186
20540
|
else
|
|
@@ -20220,6 +20574,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20220
20574
|
* hot activation contract explicit and gives tests a failure seam; no generic
|
|
20221
20575
|
* SIGHUP or caller-provided hook is ever executed. */
|
|
20222
20576
|
async runProviderSecretReloadHook(hookId, _next, _previous) {
|
|
20577
|
+
if (hookId === "cloudflare.public_link") {
|
|
20578
|
+
await this.publicWebLink?.close("connector token changed");
|
|
20579
|
+
return;
|
|
20580
|
+
}
|
|
20223
20581
|
if (hookId !== "groq.voice")
|
|
20224
20582
|
throw new Error("unknown provider secret reload hook");
|
|
20225
20583
|
const before = this.providerSecretHotSnapshots.get(hookId);
|
|
@@ -21202,7 +21560,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21202
21560
|
// up to 15s each, and on a beta build BOTH ran — 30s with no WATCHDOG ping,
|
|
21203
21561
|
// past WatchdogSec's half-interval and enough for systemd to SIGABRT the fleet
|
|
21204
21562
|
// for a background version check.
|
|
21205
|
-
const { execFile } = await import("
|
|
21563
|
+
const { execFile } = await import("./child-process.js");
|
|
21206
21564
|
const { promisify } = await import("node:util");
|
|
21207
21565
|
const execFileP = promisify(execFile);
|
|
21208
21566
|
const npmVersion = async (spec) => {
|
|
@@ -21494,10 +21852,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21494
21852
|
return;
|
|
21495
21853
|
}
|
|
21496
21854
|
const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
21855
|
+
if (!this.canvasWorkerEnabled() && isCanvasWorkerHttpPath(requestPath)) {
|
|
21856
|
+
res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
21857
|
+
res.end(JSON.stringify({ error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }));
|
|
21858
|
+
return;
|
|
21859
|
+
}
|
|
21497
21860
|
if (this.effectBootstrap.handle(req, res))
|
|
21498
21861
|
return;
|
|
21499
21862
|
// Branding is public, after Host admission: automatic favicon requests
|
|
21500
21863
|
// must not become noisy web-token 401s in the browser console.
|
|
21864
|
+
// #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
|
|
21865
|
+
useAppIconStore(this.appIcon);
|
|
21501
21866
|
if (handleWebIconRequest(req, res, requestPath))
|
|
21502
21867
|
return;
|
|
21503
21868
|
// Public: the health probe, /agent (instance-token auth of its own), the
|
|
@@ -21941,6 +22306,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21941
22306
|
return;
|
|
21942
22307
|
}
|
|
21943
22308
|
const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
|
|
22309
|
+
canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
|
|
21944
22310
|
// #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
|
|
21945
22311
|
extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
|
|
21946
22312
|
this.previewListener = listener;
|
|
@@ -22118,7 +22484,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22118
22484
|
return {};
|
|
22119
22485
|
} })(),
|
|
22120
22486
|
// #2118: only this Chat's placement content; guest projection drops global revisions.
|
|
22121
|
-
effect_chat_v: createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32),
|
|
22487
|
+
effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
|
|
22122
22488
|
// #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
|
|
22123
22489
|
color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
|
|
22124
22490
|
...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
|
|
@@ -22145,6 +22511,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22145
22511
|
};
|
|
22146
22512
|
});
|
|
22147
22513
|
return {
|
|
22514
|
+
canvas_worker: this.canvasWorkerEnabled(),
|
|
22148
22515
|
instances,
|
|
22149
22516
|
publicLink: this.getPublicWebStatus(),
|
|
22150
22517
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|