@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.11
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +16 -9
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +24 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +147 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +5 -3
- package/dist/auth-api.js +19 -3
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +8 -0
- package/dist/backend/antigravity.js +32 -9
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +39 -3
- package/dist/backend/kiro.js +143 -19
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +47 -4
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +36 -1
- package/dist/channel/adapters/discord.js +126 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +42 -2
- package/dist/channel/adapters/telegram.js +142 -23
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +26 -13
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +10 -4
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +55 -3
- package/dist/cli.js +167 -39
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +6 -3
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +59 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +29 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +156 -1
- package/dist/daemon.js +773 -89
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +70 -8
- package/dist/delivery-outbox.js +315 -24
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -1
- package/dist/fleet-manager.d.ts +404 -13
- package/dist/fleet-manager.js +2039 -210
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +29 -6
- package/dist/instance-lifecycle.js +79 -28
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +3 -1
- package/dist/instructions.js +11 -8
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-log-gate.d.ts +42 -0
- package/dist/kiro-log-gate.js +39 -0
- package/dist/kiro-log-gate.js.map +1 -0
- package/dist/locale.js +226 -44
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +99 -0
- package/dist/log-service.js +243 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +169 -0
- package/dist/log-steps.js +858 -0
- package/dist/log-steps.js.map +1 -0
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +284 -97
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +38 -15
- package/dist/outbound-schemas.js +36 -8
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +101 -0
- package/dist/public-preview.js +236 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.js +14 -4
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +24 -1
- package/dist/public-web-link.js +63 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +89 -5
- package/dist/reply-buttons-controller.js +273 -8
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +33 -0
- package/dist/restart-progress.js +30 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +46 -29
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +31 -0
- package/dist/service-installer.js +41 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +85 -25
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +72 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +28 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/tool-permissions.js +3 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +42 -2
- package/dist/topic-commands.js +101 -11
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +80 -3
- package/dist/transcript-jsonl.js +147 -10
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +19 -0
- package/dist/transcript-monitor.js +87 -15
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +78 -0
- package/dist/transcript-sources.js +375 -64
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +61 -4
- package/dist/ui/chat-store.js +37 -3
- package/dist/ui/chat-strings.js +40 -20
- package/dist/ui/chat-thread.js +111 -12
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/panel-chat.js +41 -10
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +199 -57
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +6 -2
- package/dist/ui/panel-settings.js +184 -32
- package/dist/ui/preview-panel.js +55 -15
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +44 -3
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +56 -4
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +43 -17
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +7 -2
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +26 -5
- package/dist/ui/shared/app.css +163 -22
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +241 -45
- package/dist/ui/shared/instance-switch.js +4 -4
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +32 -4
- package/dist/ui/signin.html +1 -1
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +28 -2
- package/dist/update-install.js +22 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +46 -1
- package/dist/update-launch.js +146 -3
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +34 -5
- package/dist/usage/providers.js +160 -27
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +89 -4
- package/dist/web-api.js +551 -45
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +9 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +12 -1
- package/dist/web-chat-history.js +29 -7
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +27 -4
- package/dist/web-commands.js +19 -4
- package/dist/web-commands.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +81 -0
- package/dist/web-prefs.js +297 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +50 -1
- package/dist/web-session.js +63 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +23 -0
- package/dist/web-settings-patch.js +77 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +5 -1
- package/dist/web-shell-routes.js +10 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/web-api.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { webHumanOrigin } from "./human-delivery-receipts.js";
|
|
2
|
+
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
1
3
|
import { binaryProbe } from "./binary-probe.js";
|
|
2
4
|
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
3
5
|
import { readBoundedWebBody } from "./web-body.js";
|
|
@@ -16,7 +18,9 @@ import { shellRoute } from "./web-shell-routes.js";
|
|
|
16
18
|
import { readFileSync } from "node:fs";
|
|
17
19
|
import { join, dirname } from "node:path";
|
|
18
20
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
|
|
21
|
+
import { CreateInstanceArgs, CreateScheduleArgs, UpdateScheduleArgs, validateArgs } from "./outbound-schemas.js";
|
|
22
|
+
import { scheduleView } from "./web-schedules.js";
|
|
23
|
+
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
20
24
|
import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js";
|
|
21
25
|
import { z } from "zod";
|
|
22
26
|
import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
@@ -24,8 +28,13 @@ import { dashboardOriginFor } from "./web-preview.js";
|
|
|
24
28
|
import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
|
|
25
29
|
import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
|
|
26
30
|
import { getAgendHome } from "./paths.js";
|
|
31
|
+
import { OPERATOR } from "./web-prefs.js";
|
|
32
|
+
import { WEB_NAME_FALLBACK } from "./web-profile.js";
|
|
33
|
+
import { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
|
|
34
|
+
import { parseMentionQuery } from "./web-mentions.js";
|
|
27
35
|
import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
|
|
28
36
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
37
|
+
import { resolveTz } from "./tz-utils.js";
|
|
29
38
|
// ── Strict public-facing schemas ────────────────────────────────────────────
|
|
30
39
|
// web-api endpoints must reject unknown fields so the dashboard cannot inject
|
|
31
40
|
// internal-only flags that would reach handleCreate/scheduler/config writers.
|
|
@@ -45,18 +54,17 @@ const TaskUpdateSchema = z.object({
|
|
|
45
54
|
description: z.string().max(MAX_TEXT).optional(),
|
|
46
55
|
priority: z.string().max(64).optional(),
|
|
47
56
|
}).strict();
|
|
48
|
-
const ScheduleCreateSchema = z.object({
|
|
49
|
-
cron: z.string().min(1).max(256),
|
|
50
|
-
message: z.string().min(1).max(MAX_TEXT),
|
|
51
|
-
target: z.string().min(1).max(256),
|
|
52
|
-
label: z.string().max(256).optional(),
|
|
53
|
-
timezone: z.string().max(128).optional(),
|
|
54
|
-
}).strict();
|
|
55
57
|
const TeamCreateSchema = z.object({
|
|
56
58
|
name: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/),
|
|
57
59
|
members: z.array(z.string().min(1).max(256)).min(1).max(256),
|
|
58
60
|
description: z.string().max(MAX_TEXT).optional(),
|
|
59
61
|
}).strict();
|
|
62
|
+
// #1689: an edit of a team — its members as they should be, and its description ("" clears it). No name: a team is
|
|
63
|
+
// not renamed.
|
|
64
|
+
const TeamEditSchema = z.object({
|
|
65
|
+
members: z.array(z.string().min(1).max(256)).min(1).max(256),
|
|
66
|
+
description: z.string().max(MAX_TEXT).optional(),
|
|
67
|
+
}).strict();
|
|
60
68
|
const ConfigUpdateSchema = z.object({
|
|
61
69
|
channel: z.object({
|
|
62
70
|
group_id: z.union([z.number(), z.string()]).optional(),
|
|
@@ -130,6 +138,49 @@ function json(res, status, data) {
|
|
|
130
138
|
res.writeHead(status);
|
|
131
139
|
res.end(JSON.stringify(data));
|
|
132
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* #1630 (#1635 review): an answer that waited on something (a list, a download) is sent only if the request is still
|
|
143
|
+
* allowed — the session not revoked meanwhile, a public link's exposure still current. Otherwise 401, and nothing.
|
|
144
|
+
*/
|
|
145
|
+
function stillAuthorized(req, res, url, ctx) {
|
|
146
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
147
|
+
return false;
|
|
148
|
+
if (isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false }))
|
|
149
|
+
return true;
|
|
150
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* #1827: whose a request is, asked NOW (after its body arrived: a session revoked, expired or rotated away meanwhile is
|
|
155
|
+
* no one — `null`, never the operator). An authorized session is the account it was signed in as, or (an `agend web`
|
|
156
|
+
* session, the CLI's header token) none: the operator. For recognition only; nothing here authorizes anything.
|
|
157
|
+
*/
|
|
158
|
+
function webAccountOf(req, url, ctx) {
|
|
159
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
160
|
+
if (verdict.kind !== "allow")
|
|
161
|
+
return null;
|
|
162
|
+
return verdict.via === "session" && verdict.session?.owner ? { owner: verdict.session.owner } : {};
|
|
163
|
+
}
|
|
164
|
+
function webPersonOf(req, url, ctx) {
|
|
165
|
+
// No people kept here: everyone is "web-user", as before, and there is no identity to protect.
|
|
166
|
+
if (!ctx.webPerson)
|
|
167
|
+
return { key: OPERATOR, bound: false, userId: WEB_NAME_FALLBACK, voterKey: "web:operator", name: WEB_NAME_FALLBACK, set: false };
|
|
168
|
+
const who = webAccountOf(req, url, ctx);
|
|
169
|
+
return who ? ctx.webPerson(who.owner) : null;
|
|
170
|
+
}
|
|
171
|
+
/** #1827: the person, or the request is answered 401 here (it is no longer authorized) and null is returned. */
|
|
172
|
+
function webPersonOr401(req, res, url, ctx) {
|
|
173
|
+
const p = webPersonOf(req, url, ctx);
|
|
174
|
+
if (!p)
|
|
175
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
176
|
+
return p;
|
|
177
|
+
}
|
|
178
|
+
/** #1827: what the page's profile card reads — the name they chose (or null), the one in use, and how agents know them. */
|
|
179
|
+
function profileBody(p) {
|
|
180
|
+
return { name: p.set ? p.name : null, shown: p.name, id: p.userId, bound: p.bound, ...(p.platformName !== undefined ? { platform_name: p.platformName } : {}) };
|
|
181
|
+
}
|
|
182
|
+
/** #1823: an SSE frame with the server's clock (no id: it is not a message, and a reconnect gets a fresh one). */
|
|
183
|
+
function sseClock() { return `event: clock\ndata: ${JSON.stringify({ now: Date.now() })}\n\n`; }
|
|
133
184
|
/**
|
|
134
185
|
* Handle a Web UI request. Returns true if handled, false to pass through.
|
|
135
186
|
*/
|
|
@@ -242,6 +293,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
242
293
|
// #1577: the newest message this answer accounts for — after a restart, the last restored one until this process
|
|
243
294
|
// records its own (`<boot>-0` would be after(0): the whole restored history again on every poll).
|
|
244
295
|
cursor: history ? history.pollCursor() : null,
|
|
296
|
+
// #1823 (#1830 review): the server's clock now, for the page's poll countdown (a poll's deadline is the server's).
|
|
297
|
+
now: Date.now(),
|
|
245
298
|
});
|
|
246
299
|
return true;
|
|
247
300
|
}
|
|
@@ -255,7 +308,9 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
255
308
|
"Cache-Control": "no-cache",
|
|
256
309
|
Connection: "keep-alive",
|
|
257
310
|
});
|
|
258
|
-
|
|
311
|
+
// #1823 (#1830 review): with the status, the server's clock — now and with every heartbeat — that the page counts a
|
|
312
|
+
// poll down by (one write: the frames a connect sends are unchanged in number).
|
|
313
|
+
res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n${sseClock()}`);
|
|
259
314
|
// An EventSource that reconnects says what it last saw: send what it missed, before anything new.
|
|
260
315
|
const lastSeen = parseLastEventId(req.headers["last-event-id"]);
|
|
261
316
|
if (lastSeen !== null && ctx.webChatHistory) {
|
|
@@ -291,7 +346,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
291
346
|
}
|
|
292
347
|
try {
|
|
293
348
|
// Every open dashboard tab, every 10 s: the status of every instance, serialized on the fleet's loop (#1235).
|
|
294
|
-
res.write(measureSyncWork("web.sseStatus", () => `event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`));
|
|
349
|
+
res.write(measureSyncWork("web.sseStatus", () => `event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`) + sseClock());
|
|
295
350
|
}
|
|
296
351
|
catch {
|
|
297
352
|
cleanup();
|
|
@@ -328,6 +383,45 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
328
383
|
json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
|
|
329
384
|
return true;
|
|
330
385
|
}
|
|
386
|
+
// ── #1218: the Log tab ─────────────────────────────────
|
|
387
|
+
// An instance's steps, read on demand from its own transcript (log-service.ts). Read-only, and offered on the public
|
|
388
|
+
// link on purpose (the user's decision): the same masking applies to every viewer. Passive (web-auth.ts).
|
|
389
|
+
if (method === "GET" && path === "/ui/steps") {
|
|
390
|
+
const p = url.searchParams;
|
|
391
|
+
const instance = p.get("instance") ?? "";
|
|
392
|
+
const num = (k) => {
|
|
393
|
+
const v = p.get(k);
|
|
394
|
+
if (v === null)
|
|
395
|
+
return undefined;
|
|
396
|
+
const n = Number(v);
|
|
397
|
+
return /^\d{1,15}$/.test(v) && Number.isSafeInteger(n) ? n : null;
|
|
398
|
+
};
|
|
399
|
+
const after = num("after"), before = num("before"), ev = num("ev"), limit = num("limit");
|
|
400
|
+
const key = p.get("key");
|
|
401
|
+
if (!instance || instance.length > 128 || after === null || before === null || ev === null || limit === null
|
|
402
|
+
|| (limit !== undefined && (limit < 1 || limit > 200)) || (key !== null && !/^\d{1,20}-\d{1,20}$/.test(key))) {
|
|
403
|
+
json(res, 400, { error: "instance (1-128 chars); after, before, ev: offsets; limit 1-200; key: <dev>-<ino>" });
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (!ctx.logSteps) {
|
|
407
|
+
json(res, 404, { error: "not available" });
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
ctx.logSteps(instance, {
|
|
411
|
+
...(after !== undefined ? { after } : {}), ...(before !== undefined ? { before } : {}), ...(ev !== undefined ? { ev } : {}),
|
|
412
|
+
...(limit !== undefined ? { limit } : {}), ...(key !== null ? { key } : {}),
|
|
413
|
+
skip: p.get("skip") === "1", watch: p.get("watch") === "1",
|
|
414
|
+
}).then(answer => {
|
|
415
|
+
if (answer === null)
|
|
416
|
+
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
417
|
+
else
|
|
418
|
+
json(res, 200, answer);
|
|
419
|
+
}).catch(err => {
|
|
420
|
+
ctx.logger.error({ err: err.message, instance }, "Log steps read failed");
|
|
421
|
+
json(res, 500, { error: "could not read the steps" });
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
331
425
|
// ── Send message ───────────────────────────────────────
|
|
332
426
|
if (method === "POST" && path === "/ui/send") {
|
|
333
427
|
handleSendMessage(req, res, ctx);
|
|
@@ -365,6 +459,66 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
365
459
|
res.end(bytes);
|
|
366
460
|
return true;
|
|
367
461
|
}
|
|
462
|
+
// ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
|
|
463
|
+
const imageMatch = path.match(/^\/ui\/img\/(bot|avatar|emoji)\/([^/]+)$/);
|
|
464
|
+
if (method === "GET" && imageMatch) {
|
|
465
|
+
const kind = imageMatch[1];
|
|
466
|
+
let key;
|
|
467
|
+
try {
|
|
468
|
+
key = decodeURIComponent(imageMatch[2]);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
json(res, 404, { error: "No such image" });
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
const send = (image) => {
|
|
475
|
+
if (res.headersSent || res.destroyed)
|
|
476
|
+
return;
|
|
477
|
+
if (!stillAuthorized(req, res, url, ctx))
|
|
478
|
+
return;
|
|
479
|
+
if (!image) {
|
|
480
|
+
json(res, 404, { error: "No such image" });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
res.setHeader("Content-Type", image.mime);
|
|
484
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
485
|
+
// A changed avatar is a new URL (?v=); an emoji id is one picture for good.
|
|
486
|
+
res.setHeader("Cache-Control", "private, max-age=86400");
|
|
487
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
|
|
488
|
+
res.writeHead(200);
|
|
489
|
+
res.end(image.bytes);
|
|
490
|
+
};
|
|
491
|
+
if (kind === "emoji") {
|
|
492
|
+
(ctx.webEmoji ? ctx.webEmoji(key) : Promise.resolve(null)).then(send, () => send(null));
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
send(ctx.webImage?.(kind, key) ?? null);
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (method === "GET" && path === "/ui/emojis") {
|
|
499
|
+
const answer = (emojis) => { if (!res.headersSent && !res.destroyed && stillAuthorized(req, res, url, ctx))
|
|
500
|
+
json(res, 200, { emojis }); };
|
|
501
|
+
(ctx.webEmojiList ? ctx.webEmojiList() : Promise.resolve([])).then(answer, () => answer([]));
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
// #1682: the names of the mention ids a page found in what it rendered (at most MAX_MENTION_IDS, digits only) — only
|
|
505
|
+
// those, so no page (the public link included) ever gets a directory; from memory, never a request.
|
|
506
|
+
if (method === "GET" && path === "/ui/mentions") {
|
|
507
|
+
const parsed = parseMentionQuery(url.searchParams);
|
|
508
|
+
if (!parsed.ok) {
|
|
509
|
+
json(res, 400, { error: parsed.error });
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
let names = { u: {}, r: {}, c: {} };
|
|
513
|
+
try {
|
|
514
|
+
names = ctx.webMentionNames?.(parsed.query) ?? names;
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
ctx.logger.debug({ err: err.message }, "mention names unavailable");
|
|
518
|
+
}
|
|
519
|
+
json(res, 200, names);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
368
522
|
// ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
|
|
369
523
|
if (method === "GET" && path === "/ui/prompts") {
|
|
370
524
|
json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
|
|
@@ -409,6 +563,63 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
409
563
|
});
|
|
410
564
|
return true;
|
|
411
565
|
}
|
|
566
|
+
// #1827: the person's own name — the one agents and chats see for their web messages. Their own profile, so the public
|
|
567
|
+
// link may read and set it too (profile edits are allowed there); a read-only session may as well: it is about them.
|
|
568
|
+
if (path === "/ui/profile" && method === "GET") {
|
|
569
|
+
if (!ctx.webPerson) {
|
|
570
|
+
json(res, 404, { error: "Profiles are not kept on this fleet" });
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
const p = webPersonOr401(req, res, url, ctx);
|
|
574
|
+
if (p)
|
|
575
|
+
json(res, 200, profileBody(p));
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
if (path === "/ui/profile" && method === "POST") {
|
|
579
|
+
if (!ctx.setWebPersonName) {
|
|
580
|
+
json(res, 404, { error: "Profiles are not kept on this fleet" });
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
const set = ctx.setWebPersonName.bind(ctx);
|
|
584
|
+
(async () => {
|
|
585
|
+
let body;
|
|
586
|
+
try {
|
|
587
|
+
body = await parseBody(req);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
594
|
+
return;
|
|
595
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Object.hasOwn(body, "name")) {
|
|
596
|
+
json(res, 400, { error: "name required (text, or null to clear)" });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// Whose: asked once the body is in. A session revoked, expired or rotated away meanwhile writes nothing — it is
|
|
600
|
+
// not the operator (#1833 review).
|
|
601
|
+
const who = webAccountOf(req, url, ctx);
|
|
602
|
+
if (!who) {
|
|
603
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const r = set(who.owner, body.name);
|
|
607
|
+
if (!r.ok) {
|
|
608
|
+
json(res, r.status, { error: r.error });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// The person's other devices read it at their next status (the prefs rev moved).
|
|
612
|
+
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
613
|
+
json(res, 200, profileBody(r.person));
|
|
614
|
+
})().catch(err => {
|
|
615
|
+
ctx.logger.error({ err: err.message }, "Saving the web name failed");
|
|
616
|
+
try {
|
|
617
|
+
json(res, 500, { error: "Saving the name failed" });
|
|
618
|
+
}
|
|
619
|
+
catch { /* already answered */ }
|
|
620
|
+
});
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
412
623
|
// #1266: a click on an agent reply's button. The set id travels in the body; the public link may click too (a click
|
|
413
624
|
// is a message, and the public link may send messages).
|
|
414
625
|
if (method === "POST" && path === "/ui/reply-button") {
|
|
@@ -433,8 +644,14 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
433
644
|
json(res, 400, { error: "instance, id and index required" });
|
|
434
645
|
return;
|
|
435
646
|
}
|
|
436
|
-
|
|
437
|
-
|
|
647
|
+
// #1823/#1827: the click is this page's person's — on a poll their vote (one per person, on every device of
|
|
648
|
+
// theirs), on one-click buttons their choice — under their web id and name. A request that no longer passes the
|
|
649
|
+
// gate (a session revoked meanwhile) clicks nothing as anyone.
|
|
650
|
+
const person = webPersonOr401(req, res, url, ctx);
|
|
651
|
+
if (!person)
|
|
652
|
+
return;
|
|
653
|
+
const r = await click(instance, id, index, { key: person.voterKey, userId: person.userId, name: person.name });
|
|
654
|
+
json(res, r.status, r.status === 200 ? { answered: true, ...(r.options ? { options: r.options } : {}) } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
|
|
438
655
|
})().catch(err => {
|
|
439
656
|
ctx.logger.error({ err: err.message }, "Web reply-button click failed");
|
|
440
657
|
try {
|
|
@@ -444,6 +661,97 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
444
661
|
});
|
|
445
662
|
return true;
|
|
446
663
|
}
|
|
664
|
+
// #1723: the public link page's preview, asked while it says "Preview is starting…" — its state and, once up, its
|
|
665
|
+
// origin and boot. Public link only: a local page's preview is decided by its own load.
|
|
666
|
+
if (path === "/ui/preview-status" && method === "GET") {
|
|
667
|
+
const gateway = gatewayRequestContext(req);
|
|
668
|
+
if (!gateway || !ctx.publicPreviewForUi) {
|
|
669
|
+
json(res, 404, { error: "not offered here" });
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
const p = ctx.publicPreviewForUi(gateway.exposureId, gateway.expectedOrigin);
|
|
673
|
+
json(res, 200, { code: p.code, previewOrigin: p.previewOrigin, boot: p.previewOrigin ? p.boot : null, detail: p.detail ?? null });
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
// #1665: the web preferences that follow the person between devices. Read by any page of the panel, the public link
|
|
677
|
+
// included (it reads what is in force); written only by a signed-in admin session or the CLI's header token — never
|
|
678
|
+
// over the public link (absent from its route manifest, and refused here too), never by a read-only session.
|
|
679
|
+
if (path === "/ui/prefs" && method === "GET") {
|
|
680
|
+
if (!ctx.webPrefs) {
|
|
681
|
+
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
json(res, 200, ctx.webPrefs.read(OPERATOR));
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
if (path === "/ui/prefs" && method === "POST") {
|
|
688
|
+
const prefs = ctx.webPrefs;
|
|
689
|
+
if (!prefs) {
|
|
690
|
+
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
if (gatewayRequestContext(req)) {
|
|
694
|
+
json(res, 403, { error: "prefs_not_over_public_link" });
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
// Who may write, asked now and again once the body is in: a session revoked or expired, or a token rotated, while
|
|
698
|
+
// the body was on its way writes nothing and announces nothing.
|
|
699
|
+
const writer = () => {
|
|
700
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
701
|
+
if (verdict.kind !== "allow")
|
|
702
|
+
return 401;
|
|
703
|
+
return verdict.via === "session" && verdict.session?.tier !== "admin" ? 403 : 200;
|
|
704
|
+
};
|
|
705
|
+
const refuse = (code) => json(res, code, code === 401 ? { error: WEB_TOKEN_INVALID_MESSAGE } : { error: "prefs_read_only" });
|
|
706
|
+
const first = writer();
|
|
707
|
+
if (first !== 200) {
|
|
708
|
+
refuse(first);
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
(async () => {
|
|
712
|
+
let body;
|
|
713
|
+
try {
|
|
714
|
+
body = await parseBody(req);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
721
|
+
return;
|
|
722
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
723
|
+
json(res, 400, { error: "set required" });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const parsed = prefs.parsePatch(body.set);
|
|
727
|
+
if (!parsed.ok) {
|
|
728
|
+
json(res, 400, { error: parsed.error });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const now = writer();
|
|
732
|
+
if (now !== 200) {
|
|
733
|
+
refuse(now);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const onlyIfUnset = body.onlyIfUnset === true;
|
|
737
|
+
const written = prefs.write(OPERATOR, parsed.patch, { onlyIfUnset });
|
|
738
|
+
if (!written.ok) {
|
|
739
|
+
json(res, written.status, { error: written.error });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// Another device sees the new rev in the next status frame — at once on the live stream.
|
|
743
|
+
if (written.changed.length)
|
|
744
|
+
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
745
|
+
json(res, 200, { ...written.view, changed: written.changed });
|
|
746
|
+
})().catch(err => {
|
|
747
|
+
ctx.logger.error({ err: err.message }, "Saving web preferences failed");
|
|
748
|
+
try {
|
|
749
|
+
json(res, 500, { error: "Saving preferences failed" });
|
|
750
|
+
}
|
|
751
|
+
catch { /* already answered */ }
|
|
752
|
+
});
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
447
755
|
// #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
|
|
448
756
|
// token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
|
|
449
757
|
if (method === "POST" && path === "/ui/needs/ack") {
|
|
@@ -519,7 +827,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
519
827
|
json(res, 400, { error: "invalid confirmation" });
|
|
520
828
|
return;
|
|
521
829
|
}
|
|
522
|
-
const
|
|
830
|
+
const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
|
|
831
|
+
if (!person)
|
|
832
|
+
return;
|
|
833
|
+
const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person });
|
|
523
834
|
json(res, r.status, r.body);
|
|
524
835
|
})().catch(err => {
|
|
525
836
|
ctx.logger.error({ err: err.message }, "Web command failed");
|
|
@@ -553,7 +864,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
553
864
|
json(res, 409, { error: `${name} is not running` });
|
|
554
865
|
return true;
|
|
555
866
|
}
|
|
556
|
-
ctx.eventLog?.logActivity("cancel",
|
|
867
|
+
ctx.eventLog?.logActivity("cancel", webPersonOf(req, url, ctx)?.name ?? WEB_NAME_FALLBACK, "stop the current reply", name);
|
|
557
868
|
json(res, 200, { cancelled: name });
|
|
558
869
|
return true;
|
|
559
870
|
}
|
|
@@ -586,13 +897,18 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
586
897
|
}
|
|
587
898
|
const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
|
|
588
899
|
(async () => {
|
|
900
|
+
let held = false;
|
|
589
901
|
try {
|
|
590
|
-
await ctx.startInstance(name, config, topicMode ?? false);
|
|
902
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
|
|
591
903
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
592
|
-
|
|
904
|
+
if (!held)
|
|
905
|
+
json(res, 200, { started: name });
|
|
593
906
|
}
|
|
594
907
|
catch (err) {
|
|
595
|
-
|
|
908
|
+
if (!held)
|
|
909
|
+
json(res, 500, { error: err.message });
|
|
910
|
+
else
|
|
911
|
+
ctx.logger.error({ err, name }, "Held instance action failed");
|
|
596
912
|
}
|
|
597
913
|
})();
|
|
598
914
|
return true;
|
|
@@ -705,14 +1021,19 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
705
1021
|
if (method === "POST" && restartMatch) {
|
|
706
1022
|
const name = decodeURIComponent(restartMatch[1]);
|
|
707
1023
|
(async () => {
|
|
1024
|
+
let held = false;
|
|
708
1025
|
try {
|
|
709
|
-
await ctx.restartSingleInstance(name, { explicit: true });
|
|
1026
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.restartSingleInstance(name, { explicit: true }));
|
|
710
1027
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
711
|
-
|
|
1028
|
+
if (!held)
|
|
1029
|
+
json(res, 200, { restarted: name });
|
|
712
1030
|
}
|
|
713
1031
|
catch (err) {
|
|
714
1032
|
const status = err.message.includes("not found") ? 404 : 500;
|
|
715
|
-
|
|
1033
|
+
if (!held)
|
|
1034
|
+
json(res, status, { error: err.message });
|
|
1035
|
+
else
|
|
1036
|
+
ctx.logger.error({ err, name }, "Held instance action failed");
|
|
716
1037
|
}
|
|
717
1038
|
})();
|
|
718
1039
|
return true;
|
|
@@ -774,12 +1095,15 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
774
1095
|
const parsed = parseOrReject(TaskCreateSchema, body, res);
|
|
775
1096
|
if (!parsed)
|
|
776
1097
|
return;
|
|
1098
|
+
const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
|
|
1099
|
+
if (!person)
|
|
1100
|
+
return;
|
|
777
1101
|
const task = ctx.scheduler.db.createTask({
|
|
778
1102
|
title: parsed.title,
|
|
779
1103
|
description: parsed.description,
|
|
780
1104
|
priority: parsed.priority,
|
|
781
1105
|
assignee: parsed.assignee,
|
|
782
|
-
created_by:
|
|
1106
|
+
created_by: person.name, // #1827
|
|
783
1107
|
});
|
|
784
1108
|
json(res, 200, task);
|
|
785
1109
|
}
|
|
@@ -806,7 +1130,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
806
1130
|
return;
|
|
807
1131
|
let result;
|
|
808
1132
|
if (parsed.action === "claim") {
|
|
809
|
-
|
|
1133
|
+
const person = parsed.assignee ? null : webPersonOr401(req, res, url, ctx); // #1827: the default is the person
|
|
1134
|
+
if (!parsed.assignee && !person)
|
|
1135
|
+
return;
|
|
1136
|
+
result = ctx.scheduler.db.claimTask(id, parsed.assignee || person.name);
|
|
810
1137
|
}
|
|
811
1138
|
else if (parsed.action === "complete") {
|
|
812
1139
|
result = ctx.scheduler.db.completeTask(id, parsed.result);
|
|
@@ -830,10 +1157,28 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
830
1157
|
json(res, 200, { schedules: [] });
|
|
831
1158
|
return true;
|
|
832
1159
|
}
|
|
833
|
-
|
|
1160
|
+
// #1724: each row with its next run and one-shot time in the fleet's zone — worked out here, not on the page.
|
|
1161
|
+
const scheduler = ctx.scheduler;
|
|
1162
|
+
const now = Date.now();
|
|
1163
|
+
json(res, 200, {
|
|
1164
|
+
schedules: scheduler.list().map((s) => scheduleView(s, scheduler.getRetry?.(s.id) ?? null, now)),
|
|
1165
|
+
// The zone a new schedule (and a "Once" date and time typed without one) is read in.
|
|
1166
|
+
default_timezone: ctx.fleetConfig?.defaults?.scheduler?.default_timezone
|
|
1167
|
+
?? DEFAULT_SCHEDULER_CONFIG.default_timezone,
|
|
1168
|
+
});
|
|
834
1169
|
return true;
|
|
835
1170
|
}
|
|
1171
|
+
// #1724: who may write a schedule, asked again once the body is in (a session revoked meanwhile, a non-admin tier).
|
|
1172
|
+
const scheduleWriterGone = () => {
|
|
1173
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1174
|
+
return verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin");
|
|
1175
|
+
};
|
|
836
1176
|
if (method === "POST" && path === "/ui/schedules") {
|
|
1177
|
+
// #1724: schedules are not created, edited or deleted over the public link (absent from its manifest too, #1689).
|
|
1178
|
+
if (gatewayRequestContext(req)) {
|
|
1179
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
837
1182
|
if (!ctx.scheduler) {
|
|
838
1183
|
json(res, 500, { error: "Scheduler not initialized" });
|
|
839
1184
|
return true;
|
|
@@ -843,10 +1188,23 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
843
1188
|
const body = await parseBody(req);
|
|
844
1189
|
if (!permitWebContinuation(req, res, ctx))
|
|
845
1190
|
return;
|
|
846
|
-
|
|
1191
|
+
if (scheduleWriterGone()) {
|
|
1192
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
// #1724: the create_schedule tool's own schema — cron or at (exactly one) — plus a target, which the web names.
|
|
1196
|
+
const parsed = parseOrReject(CreateScheduleArgs, body, res);
|
|
847
1197
|
if (!parsed)
|
|
848
1198
|
return;
|
|
849
|
-
|
|
1199
|
+
if (typeof parsed.target !== "string" || !parsed.target) {
|
|
1200
|
+
json(res, 400, { error: "target: an instance is required" });
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const schedule = ctx.scheduler.create({
|
|
1204
|
+
cron: parsed.cron, at: parsed.at, message: parsed.message, target: parsed.target, label: parsed.label, timezone: parsed.timezone,
|
|
1205
|
+
...(parsed.silent !== undefined ? { silent: parsed.silent } : {}),
|
|
1206
|
+
source: "web", reply_chat_id: "", reply_thread_id: null,
|
|
1207
|
+
});
|
|
850
1208
|
json(res, 200, schedule);
|
|
851
1209
|
}
|
|
852
1210
|
catch (err) {
|
|
@@ -856,7 +1214,48 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
856
1214
|
return true;
|
|
857
1215
|
}
|
|
858
1216
|
const schedDelMatch = path.match(/^\/ui\/schedules\/(.+)$/);
|
|
1217
|
+
// #1724: Edit — the update_schedule tool's validation (UpdateScheduleArgs) and its call (scheduler.update): cron and at
|
|
1218
|
+
// clear each other there; silent is not touched.
|
|
1219
|
+
if (method === "PATCH" && schedDelMatch) {
|
|
1220
|
+
if (gatewayRequestContext(req)) {
|
|
1221
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
if (!ctx.scheduler || typeof ctx.scheduler.update !== "function") {
|
|
1225
|
+
json(res, 500, { error: "Scheduler not initialized" });
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
const id = decodeURIComponent(schedDelMatch[1]);
|
|
1229
|
+
(async () => {
|
|
1230
|
+
try {
|
|
1231
|
+
const body = await parseBody(req);
|
|
1232
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1233
|
+
return;
|
|
1234
|
+
if (scheduleWriterGone()) {
|
|
1235
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.hasOwn(body, "id")) {
|
|
1239
|
+
json(res, 400, { error: "expected the fields to change (the id is the path's)" });
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const parsed = parseOrReject(UpdateScheduleArgs, { ...body, id }, res);
|
|
1243
|
+
if (!parsed)
|
|
1244
|
+
return;
|
|
1245
|
+
const { id: _id, ...changes } = parsed;
|
|
1246
|
+
json(res, 200, ctx.scheduler.update(id, changes));
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
json(res, 400, { error: err.message });
|
|
1250
|
+
}
|
|
1251
|
+
})();
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
859
1254
|
if (method === "DELETE" && schedDelMatch) {
|
|
1255
|
+
if (gatewayRequestContext(req)) {
|
|
1256
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1257
|
+
return true;
|
|
1258
|
+
}
|
|
860
1259
|
if (!ctx.scheduler) {
|
|
861
1260
|
json(res, 500, { error: "Scheduler not initialized" });
|
|
862
1261
|
return true;
|
|
@@ -893,26 +1292,82 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
893
1292
|
return true;
|
|
894
1293
|
}
|
|
895
1294
|
if (method === "POST" && path === "/ui/teams") {
|
|
1295
|
+
// #1689: not over the public link (absent from its manifest too), like editing and deleting a team.
|
|
1296
|
+
if (gatewayRequestContext(req)) {
|
|
1297
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
896
1300
|
(async () => {
|
|
897
1301
|
try {
|
|
898
1302
|
const body = await parseBody(req);
|
|
899
1303
|
if (!permitWebContinuation(req, res, ctx))
|
|
900
1304
|
return;
|
|
1305
|
+
// #1692: who may write, asked again once the body is in (as the edit does).
|
|
1306
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1307
|
+
if (verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin")) {
|
|
1308
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
901
1311
|
const parsed = parseOrReject(TeamCreateSchema, body, res);
|
|
902
1312
|
if (!parsed)
|
|
903
1313
|
return;
|
|
904
|
-
|
|
905
|
-
|
|
1314
|
+
// #1692: the create_team path — members checked, an existing team never overwritten, a failed save undone.
|
|
1315
|
+
const r = commitTeamCreate(ctx.fleetConfig, parsed, () => ctx.saveFleetConfig());
|
|
1316
|
+
if (!r.ok) {
|
|
1317
|
+
json(res, r.status, { error: r.error });
|
|
906
1318
|
return;
|
|
907
1319
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1320
|
+
json(res, 200, r.result);
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
json(res, 400, { error: err.message });
|
|
1324
|
+
}
|
|
1325
|
+
})();
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
// #1689: edit a team — the same update as the update_team tool (applyTeamUpdate), from the members the page sends:
|
|
1329
|
+
// what to add and remove is worked out against the team as it is now. Never over the public link.
|
|
1330
|
+
const teamEditMatch = path.match(/^\/ui\/teams\/(.+)$/);
|
|
1331
|
+
if ((method === "PATCH" || method === "PUT") && teamEditMatch) {
|
|
1332
|
+
if (gatewayRequestContext(req)) {
|
|
1333
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
const name = decodeURIComponent(teamEditMatch[1]);
|
|
1337
|
+
// Who may write, asked again once the body is in (#1693 review): a session revoked or a token rotated meanwhile
|
|
1338
|
+
// writes nothing.
|
|
1339
|
+
const writerNow = () => {
|
|
1340
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1341
|
+
return verdict.kind === "allow" && !(verdict.via === "session" && verdict.session?.tier !== "admin");
|
|
1342
|
+
};
|
|
1343
|
+
(async () => {
|
|
1344
|
+
try {
|
|
1345
|
+
const body = await parseBody(req);
|
|
1346
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1347
|
+
return;
|
|
1348
|
+
if (!writerNow()) {
|
|
1349
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const parsed = parseOrReject(TeamEditSchema, body, res);
|
|
1353
|
+
if (!parsed)
|
|
1354
|
+
return;
|
|
1355
|
+
const teams = ctx.fleetConfig?.teams;
|
|
1356
|
+
const current = teams && Object.hasOwn(teams, name) ? teams[name].members : null;
|
|
1357
|
+
if (!current) {
|
|
1358
|
+
json(res, 404, { error: `Team not found: ${name}` });
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const want = [...new Set(parsed.members)];
|
|
1362
|
+
// The update and its save as one: a failed save leaves the team as it was (500).
|
|
1363
|
+
const r = commitTeamUpdate(ctx.fleetConfig, {
|
|
1364
|
+
name, add: want.filter(m => !current.includes(m)), remove: current.filter(m => !want.includes(m)), description: parsed.description,
|
|
1365
|
+
}, () => ctx.saveFleetConfig());
|
|
1366
|
+
if (!r.ok) {
|
|
1367
|
+
json(res, r.status, { error: r.error });
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
json(res, 200, { updated: r.result });
|
|
916
1371
|
}
|
|
917
1372
|
catch (err) {
|
|
918
1373
|
json(res, 400, { error: err.message });
|
|
@@ -922,8 +1377,13 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
922
1377
|
}
|
|
923
1378
|
const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
|
|
924
1379
|
if (method === "DELETE" && teamDelMatch) {
|
|
1380
|
+
if (gatewayRequestContext(req)) {
|
|
1381
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
925
1384
|
const name = decodeURIComponent(teamDelMatch[1]);
|
|
926
|
-
|
|
1385
|
+
// Own keys (#1689): a team is one fleet.yaml defines — "__proto__" or "constructor" is not found.
|
|
1386
|
+
if (!ctx.fleetConfig?.teams || !Object.hasOwn(ctx.fleetConfig.teams, name)) {
|
|
927
1387
|
json(res, 404, { error: `Team not found: ${name}` });
|
|
928
1388
|
return true;
|
|
929
1389
|
}
|
|
@@ -1062,6 +1522,14 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1062
1522
|
return;
|
|
1063
1523
|
}
|
|
1064
1524
|
const message = typed;
|
|
1525
|
+
// #1827: who is writing — the name agents and chats see, and the id they tell people apart by. Asked now that the
|
|
1526
|
+
// body is in: a session revoked meanwhile sends nothing (not as the operator).
|
|
1527
|
+
const person = webPersonOf(req, new URL(req.url ?? "/", "http://localhost"), ctx);
|
|
1528
|
+
if (!person) {
|
|
1529
|
+
ctx.webFiles?.release(files);
|
|
1530
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1065
1533
|
const ts = new Date().toISOString();
|
|
1066
1534
|
// The id the agent is given, and the one its delivery reports come back under: the page's ticks.
|
|
1067
1535
|
const messageId = newWebMessageId();
|
|
@@ -1080,7 +1548,7 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1080
1548
|
// opt-in resolved at send time inside sendClassicWebEcho.
|
|
1081
1549
|
const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
|
|
1082
1550
|
const settleClassicEcho = canClassicEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
1083
|
-
await ctx.sendClassicWebEcho(instance, formatWebChannelEcho(
|
|
1551
|
+
await ctx.sendClassicWebEcho(instance, formatWebChannelEcho(person.name, preview, t("web.echo_full_text")), person.name);
|
|
1084
1552
|
}) : undefined;
|
|
1085
1553
|
const settleEcho = canEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
1086
1554
|
// A replacement adapter or edited binding is not the route we reserved.
|
|
@@ -1088,18 +1556,21 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1088
1556
|
|| (ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "")) !== groupId
|
|
1089
1557
|
|| ctx.fleetConfig?.instances[instance]?.topic_id !== topicId)
|
|
1090
1558
|
return;
|
|
1091
|
-
return syncAdapter.sendText(String(groupId), formatWebChannelEcho(
|
|
1559
|
+
return syncAdapter.sendText(String(groupId), formatWebChannelEcho(person.name, preview, t("web.echo_full_text")), { threadId: String(topicId), format: "text", allowedMentions: { parse: [] } });
|
|
1092
1560
|
}) : undefined;
|
|
1093
1561
|
let delivered;
|
|
1094
1562
|
try {
|
|
1095
|
-
|
|
1563
|
+
const deliver = (name, payload) => ctx.deliverHumanToInstance
|
|
1564
|
+
? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
|
|
1565
|
+
: ctx.deliverToInstance(name, payload);
|
|
1566
|
+
delivered = await deliver(instance, {
|
|
1096
1567
|
type: "fleet_inbound",
|
|
1097
1568
|
content: delivery.text,
|
|
1098
1569
|
targetSession: instance,
|
|
1099
1570
|
meta: {
|
|
1100
1571
|
chat_id: groupId ? String(groupId) : "",
|
|
1101
1572
|
message_id: messageId,
|
|
1102
|
-
user:
|
|
1573
|
+
user: person.name, user_id: person.userId,
|
|
1103
1574
|
ts,
|
|
1104
1575
|
thread_id: topicId != null ? String(topicId) : "",
|
|
1105
1576
|
source: "web",
|
|
@@ -1129,10 +1600,17 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1129
1600
|
}
|
|
1130
1601
|
settleEcho?.(true);
|
|
1131
1602
|
settleClassicEcho?.(true);
|
|
1603
|
+
// #1666: the same cancel/progress bubble a platform inbound starts, in the topic the echo went to — a second turn
|
|
1604
|
+
// on the echo's lane, so the topic reads "echo, then bubble" and a cancel in between drops it (the lane's epoch).
|
|
1605
|
+
// Not a copy (#1668): a slow bubble never drops the next message's echo, and a slow echo never drops the bubble.
|
|
1606
|
+
if (settleEcho && ctx.startWebChatProgress) {
|
|
1607
|
+
const startProgress = ctx.startWebChatProgress.bind(ctx);
|
|
1608
|
+
ctx.reserveWebChannelEcho?.(instance, () => startProgress(instance), { copy: false })(true);
|
|
1609
|
+
}
|
|
1132
1610
|
ctx.webFiles?.commit(files);
|
|
1133
|
-
ctx.lastInboundUser.set(instance,
|
|
1134
|
-
ctx.eventLog?.logActivity("message",
|
|
1135
|
-
ctx.emitSseEvent("message", { instance, sender:
|
|
1611
|
+
ctx.lastInboundUser.set(instance, person.name);
|
|
1612
|
+
ctx.eventLog?.logActivity("message", person.name, (message || `[${files.length} file(s)]`).slice(0, 200), instance);
|
|
1613
|
+
ctx.emitSseEvent("message", { instance, sender: person.name, role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId });
|
|
1136
1614
|
json(res, 200, { sent: true, messageId });
|
|
1137
1615
|
}
|
|
1138
1616
|
catch {
|
|
@@ -1209,8 +1687,12 @@ function handleUpload(req, res, url, ctx) {
|
|
|
1209
1687
|
});
|
|
1210
1688
|
}
|
|
1211
1689
|
function shellPreview(req, ctx, mode) {
|
|
1212
|
-
if (mode !== "full"
|
|
1690
|
+
if (mode !== "full")
|
|
1213
1691
|
return null;
|
|
1692
|
+
// #1723: a page of the public link asks its own exposure's preview — never the local listener's Host-based choice.
|
|
1693
|
+
const gateway = gatewayRequestContext(req);
|
|
1694
|
+
if (gateway)
|
|
1695
|
+
return ctx.publicPreviewForUi?.(gateway.exposureId, gateway.expectedOrigin) ?? null;
|
|
1214
1696
|
return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
|
|
1215
1697
|
}
|
|
1216
1698
|
/**
|
|
@@ -1259,6 +1741,21 @@ function serveMediaRange(req, res, files, id, file) {
|
|
|
1259
1741
|
res.writeHead(ranged ? 206 : 200);
|
|
1260
1742
|
res.end(got.bytes);
|
|
1261
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* #1665: `<html>` carrying the host's preferences (data-prefs: `{"rev":n,"values":{…}}`, attribute-escaped). theme.js (a
|
|
1746
|
+
* blocking script in <head>) and the app's dictionary take the theme and language from it over the browser's cache,
|
|
1747
|
+
* and app-prefs.js copies every value into that cache before any panel reads it — so no device paints a stale theme
|
|
1748
|
+
* or replays a tour already seen, a new device included. Only for the signed-in app (the public link reads what is in
|
|
1749
|
+
* force too); the anonymous View reader and the sign-in page keep the browser's own.
|
|
1750
|
+
*/
|
|
1751
|
+
function shellHtmlTag(ctx, mode) {
|
|
1752
|
+
if (mode !== "full" || !ctx.webPrefs)
|
|
1753
|
+
return '<html lang="en">';
|
|
1754
|
+
const view = ctx.webPrefs.read(OPERATOR);
|
|
1755
|
+
const lang = view.values.lang === "zh-TW" ? "zh-Hant" : "en";
|
|
1756
|
+
const attr = (v) => v.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
1757
|
+
return `<html lang="${lang}" data-prefs="${attr(JSON.stringify({ rev: view.rev, values: view.values }))}">`;
|
|
1758
|
+
}
|
|
1262
1759
|
function shellBodyTag(req, ctx, mode) {
|
|
1263
1760
|
const p = shellPreview(req, ctx, mode);
|
|
1264
1761
|
const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
@@ -1266,8 +1763,14 @@ function shellBodyTag(req, ctx, mode) {
|
|
|
1266
1763
|
+ ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
|
|
1267
1764
|
// #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
|
|
1268
1765
|
+ ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
|
|
1766
|
+
// #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
|
|
1767
|
+
+ `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
|
|
1269
1768
|
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
|
|
1270
1769
|
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
|
|
1770
|
+
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
1771
|
+
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
1772
|
+
// #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
|
|
1773
|
+
+ ` data-fleet-tz="${attr(resolveTz())}"`
|
|
1271
1774
|
// #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
|
|
1272
1775
|
+ `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}>`;
|
|
1273
1776
|
}
|
|
@@ -1282,7 +1785,10 @@ export function serveAppShell(req, res, ctx, mode) {
|
|
|
1282
1785
|
const own = mode === "full" && !gatewayRequestContext(req)
|
|
1283
1786
|
? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) : null;
|
|
1284
1787
|
const mediaSrc = own ? `${own}/ui/file/` : "'none'";
|
|
1285
|
-
|
|
1788
|
+
// A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
|
|
1789
|
+
// the page may load); every other page frames exactly the origin this load chose (#1723).
|
|
1790
|
+
const frameSrc = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
|
|
1791
|
+
sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode)).replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
1286
1792
|
}
|
|
1287
1793
|
catch {
|
|
1288
1794
|
json(res, 500, { error: "app.html not found" });
|