@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.10

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.
Files changed (377) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +21 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +2 -2
  12. package/dist/auth-api.js +4 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +3 -0
  15. package/dist/backend/antigravity.js +9 -0
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +1 -1
  33. package/dist/backend/effort-metadata.js +3 -2
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +33 -0
  48. package/dist/backend/launch-effort.js +123 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +33 -4
  54. package/dist/backend/types.js +2 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +24 -1
  66. package/dist/channel/adapters/discord.js +87 -2
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +24 -2
  69. package/dist/channel/adapters/telegram.js +118 -20
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +24 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.js +9 -3
  83. package/dist/channel/tool-router.js.map +1 -1
  84. package/dist/channel/types.d.ts +39 -3
  85. package/dist/cli.js +133 -28
  86. package/dist/cli.js.map +1 -1
  87. package/dist/command-table.d.ts +2 -2
  88. package/dist/command-table.js +6 -3
  89. package/dist/command-table.js.map +1 -1
  90. package/dist/config-validator.js +59 -0
  91. package/dist/config-validator.js.map +1 -1
  92. package/dist/cross-instance-envelope.d.ts +15 -2
  93. package/dist/cross-instance-envelope.js +29 -5
  94. package/dist/cross-instance-envelope.js.map +1 -1
  95. package/dist/daemon.d.ts +156 -1
  96. package/dist/daemon.js +764 -89
  97. package/dist/daemon.js.map +1 -1
  98. package/dist/delivery-outbox.d.ts +62 -7
  99. package/dist/delivery-outbox.js +309 -22
  100. package/dist/delivery-outbox.js.map +1 -1
  101. package/dist/delivery-reconciliation.d.ts +18 -1
  102. package/dist/delivery-reconciliation.js +67 -2
  103. package/dist/delivery-reconciliation.js.map +1 -1
  104. package/dist/fleet-context.d.ts +10 -1
  105. package/dist/fleet-manager.d.ts +345 -10
  106. package/dist/fleet-manager.js +1868 -174
  107. package/dist/fleet-manager.js.map +1 -1
  108. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  111. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  112. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  113. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  114. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  115. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  116. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  117. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  118. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  119. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  120. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  121. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  123. package/dist/general-knowledge/steering/core-rules.md +8 -7
  124. package/dist/host-memory.d.ts +10 -0
  125. package/dist/host-memory.js +33 -1
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/human-delivery-receipts.d.ts +83 -0
  128. package/dist/human-delivery-receipts.js +194 -0
  129. package/dist/human-delivery-receipts.js.map +1 -0
  130. package/dist/instance-avatar.d.ts +36 -0
  131. package/dist/instance-avatar.js +57 -0
  132. package/dist/instance-avatar.js.map +1 -0
  133. package/dist/instance-config-impact.js +6 -0
  134. package/dist/instance-config-impact.js.map +1 -1
  135. package/dist/instance-lifecycle.d.ts +29 -6
  136. package/dist/instance-lifecycle.js +79 -28
  137. package/dist/instance-lifecycle.js.map +1 -1
  138. package/dist/instructions.d.ts +3 -1
  139. package/dist/instructions.js +11 -8
  140. package/dist/instructions.js.map +1 -1
  141. package/dist/kiro-log-gate.d.ts +42 -0
  142. package/dist/kiro-log-gate.js +39 -0
  143. package/dist/kiro-log-gate.js.map +1 -0
  144. package/dist/locale.js +200 -44
  145. package/dist/locale.js.map +1 -1
  146. package/dist/log-service.d.ts +99 -0
  147. package/dist/log-service.js +243 -0
  148. package/dist/log-service.js.map +1 -0
  149. package/dist/log-steps.d.ts +169 -0
  150. package/dist/log-steps.js +858 -0
  151. package/dist/log-steps.js.map +1 -0
  152. package/dist/login-controller.d.ts +20 -3
  153. package/dist/login-controller.js +71 -19
  154. package/dist/login-controller.js.map +1 -1
  155. package/dist/login-flows.d.ts +5 -2
  156. package/dist/login-flows.js +5 -4
  157. package/dist/login-flows.js.map +1 -1
  158. package/dist/memory-pressure.d.ts +13 -1
  159. package/dist/memory-pressure.js +50 -10
  160. package/dist/memory-pressure.js.map +1 -1
  161. package/dist/needs-you-hub.js +2 -0
  162. package/dist/needs-you-hub.js.map +1 -1
  163. package/dist/needs-you-live.d.ts +6 -0
  164. package/dist/needs-you-live.js +23 -3
  165. package/dist/needs-you-live.js.map +1 -1
  166. package/dist/next-steps.js +1 -1
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/outbound-handlers.d.ts +100 -0
  169. package/dist/outbound-handlers.js +263 -96
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +26 -15
  172. package/dist/outbound-schemas.js +24 -7
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-link-progress.d.ts +1 -1
  175. package/dist/public-link-progress.js +8 -2
  176. package/dist/public-link-progress.js.map +1 -1
  177. package/dist/public-preview.d.ts +101 -0
  178. package/dist/public-preview.js +236 -0
  179. package/dist/public-preview.js.map +1 -0
  180. package/dist/public-web-gateway.js +11 -4
  181. package/dist/public-web-gateway.js.map +1 -1
  182. package/dist/public-web-link.d.ts +24 -1
  183. package/dist/public-web-link.js +63 -9
  184. package/dist/public-web-link.js.map +1 -1
  185. package/dist/reply-buttons-controller.d.ts +3 -0
  186. package/dist/reply-buttons-controller.js +3 -2
  187. package/dist/reply-buttons-controller.js.map +1 -1
  188. package/dist/restart-guard.d.ts +6 -3
  189. package/dist/restart-guard.js +6 -3
  190. package/dist/restart-guard.js.map +1 -1
  191. package/dist/restart-progress.d.ts +33 -0
  192. package/dist/restart-progress.js +30 -0
  193. package/dist/restart-progress.js.map +1 -1
  194. package/dist/service-activation.d.ts +15 -7
  195. package/dist/service-activation.js +46 -29
  196. package/dist/service-activation.js.map +1 -1
  197. package/dist/service-installer.d.ts +31 -0
  198. package/dist/service-installer.js +41 -2
  199. package/dist/service-installer.js.map +1 -1
  200. package/dist/settings-api.js +85 -25
  201. package/dist/settings-api.js.map +1 -1
  202. package/dist/settings-change.js +72 -1
  203. package/dist/settings-change.js.map +1 -1
  204. package/dist/settings-effect.js +28 -4
  205. package/dist/settings-effect.js.map +1 -1
  206. package/dist/settings-http-confirmation.js +7 -0
  207. package/dist/settings-http-confirmation.js.map +1 -1
  208. package/dist/setup-form.d.ts +1 -1
  209. package/dist/setup-form.js +1 -1
  210. package/dist/spawn-gate.js +5 -1
  211. package/dist/spawn-gate.js.map +1 -1
  212. package/dist/spawn-hold-reply.d.ts +12 -0
  213. package/dist/spawn-hold-reply.js +39 -0
  214. package/dist/spawn-hold-reply.js.map +1 -0
  215. package/dist/status-emojis.d.ts +10 -3
  216. package/dist/status-emojis.js +11 -2
  217. package/dist/status-emojis.js.map +1 -1
  218. package/dist/systemd-peer-bus.d.ts +95 -0
  219. package/dist/systemd-peer-bus.js +430 -0
  220. package/dist/systemd-peer-bus.js.map +1 -0
  221. package/dist/systemd-runtime.d.ts +27 -1
  222. package/dist/systemd-runtime.js +69 -7
  223. package/dist/systemd-runtime.js.map +1 -1
  224. package/dist/tips.js +4 -4
  225. package/dist/tips.js.map +1 -1
  226. package/dist/tool-progress.js +3 -2
  227. package/dist/tool-progress.js.map +1 -1
  228. package/dist/topic-commands.d.ts +42 -2
  229. package/dist/topic-commands.js +92 -7
  230. package/dist/topic-commands.js.map +1 -1
  231. package/dist/transcript-delta-reader.d.ts +3 -3
  232. package/dist/transcript-delta-reader.js +8 -8
  233. package/dist/transcript-delta-reader.js.map +1 -1
  234. package/dist/transcript-jsonl.d.ts +80 -3
  235. package/dist/transcript-jsonl.js +147 -10
  236. package/dist/transcript-jsonl.js.map +1 -1
  237. package/dist/transcript-monitor.d.ts +19 -0
  238. package/dist/transcript-monitor.js +87 -15
  239. package/dist/transcript-monitor.js.map +1 -1
  240. package/dist/transcript-sources.d.ts +78 -0
  241. package/dist/transcript-sources.js +375 -64
  242. package/dist/transcript-sources.js.map +1 -1
  243. package/dist/transcript-turns.d.ts +15 -2
  244. package/dist/transcript-turns.js +15 -2
  245. package/dist/transcript-turns.js.map +1 -1
  246. package/dist/tunnel/cloudflared.d.ts +7 -15
  247. package/dist/tunnel/cloudflared.js +36 -1
  248. package/dist/tunnel/cloudflared.js.map +1 -1
  249. package/dist/tunnel/purpose-lane.d.ts +5 -1
  250. package/dist/tunnel/purpose-lane.js +14 -7
  251. package/dist/tunnel/purpose-lane.js.map +1 -1
  252. package/dist/tunnel/types.d.ts +5 -1
  253. package/dist/tunnel/types.js.map +1 -1
  254. package/dist/turn-resume.d.ts +2 -0
  255. package/dist/turn-resume.js +1 -1
  256. package/dist/turn-resume.js.map +1 -1
  257. package/dist/types.d.ts +25 -0
  258. package/dist/ui/chat-commands.js +1 -0
  259. package/dist/ui/chat-emojis.js +33 -0
  260. package/dist/ui/chat-mentions.js +63 -0
  261. package/dist/ui/chat-render.js +42 -2
  262. package/dist/ui/chat-store.js +2 -2
  263. package/dist/ui/chat-strings.js +38 -20
  264. package/dist/ui/chat-thread.js +69 -12
  265. package/dist/ui/chat-time.js +80 -0
  266. package/dist/ui/panel-chat.js +39 -10
  267. package/dist/ui/panel-details.js +2 -0
  268. package/dist/ui/panel-fleet.js +199 -57
  269. package/dist/ui/panel-log.js +428 -0
  270. package/dist/ui/panel-needs.js +6 -2
  271. package/dist/ui/panel-settings.js +137 -32
  272. package/dist/ui/preview-panel.js +55 -15
  273. package/dist/ui/preview-status.js +28 -0
  274. package/dist/ui/preview.js +44 -3
  275. package/dist/ui/settings-dialogs.js +7 -2
  276. package/dist/ui/settings-model.js +2 -1
  277. package/dist/ui/settings-strings.js +46 -4
  278. package/dist/ui/settings-token.js +1 -1
  279. package/dist/ui/shared/app-i18n.js +43 -17
  280. package/dist/ui/shared/app-prefs.js +250 -0
  281. package/dist/ui/shared/app-route.js +7 -2
  282. package/dist/ui/shared/app-shell.js +165 -8
  283. package/dist/ui/shared/app-store.js +5 -1
  284. package/dist/ui/shared/app-stream.js +23 -5
  285. package/dist/ui/shared/app.css +155 -22
  286. package/dist/ui/shared/app.js +11 -1
  287. package/dist/ui/shared/fonts/README.md +1 -1
  288. package/dist/ui/shared/header-tools.js +55 -5
  289. package/dist/ui/shared/instance-nav.js +241 -45
  290. package/dist/ui/shared/instance-switch.js +4 -4
  291. package/dist/ui/shared/panel-view.js +6 -0
  292. package/dist/ui/shared/signin.js +7 -7
  293. package/dist/ui/shared/theme.js +39 -7
  294. package/dist/ui/shared/tokens.css +1 -1
  295. package/dist/ui/shared/ui-icons.js +1 -0
  296. package/dist/ui/shared/usage-dialog.js +23 -5
  297. package/dist/ui/shared/view-strings.js +32 -4
  298. package/dist/ui/signin.html +1 -1
  299. package/dist/unrecognised-prompt.d.ts +23 -0
  300. package/dist/unrecognised-prompt.js +74 -0
  301. package/dist/unrecognised-prompt.js.map +1 -0
  302. package/dist/update-install.d.ts +28 -2
  303. package/dist/update-install.js +22 -4
  304. package/dist/update-install.js.map +1 -1
  305. package/dist/update-marker.d.ts +51 -0
  306. package/dist/update-marker.js +61 -0
  307. package/dist/update-marker.js.map +1 -1
  308. package/dist/update-preflight.d.ts +100 -0
  309. package/dist/update-preflight.js +264 -0
  310. package/dist/update-preflight.js.map +1 -0
  311. package/dist/updater-caller.d.ts +31 -0
  312. package/dist/updater-caller.js +109 -0
  313. package/dist/updater-caller.js.map +1 -0
  314. package/dist/usage/i18n-keys.d.ts +1 -1
  315. package/dist/usage/i18n-keys.js +1 -1
  316. package/dist/usage/i18n-keys.js.map +1 -1
  317. package/dist/usage/providers.d.ts +34 -5
  318. package/dist/usage/providers.js +160 -27
  319. package/dist/usage/providers.js.map +1 -1
  320. package/dist/usage/statusline-usage.d.ts +6 -2
  321. package/dist/usage/statusline-usage.js +10 -4
  322. package/dist/usage/statusline-usage.js.map +1 -1
  323. package/dist/view-api.d.ts +1 -1
  324. package/dist/view-api.js +38 -8
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/wake-coordinator.d.ts +11 -0
  327. package/dist/wake-coordinator.js +38 -4
  328. package/dist/wake-coordinator.js.map +1 -1
  329. package/dist/web-api.d.ts +49 -2
  330. package/dist/web-api.js +421 -31
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.d.ts +2 -2
  333. package/dist/web-auth.js +9 -4
  334. package/dist/web-auth.js.map +1 -1
  335. package/dist/web-commands.d.ts +27 -4
  336. package/dist/web-commands.js +19 -4
  337. package/dist/web-commands.js.map +1 -1
  338. package/dist/web-host-guard.d.ts +3 -11
  339. package/dist/web-host-guard.js +3 -12
  340. package/dist/web-host-guard.js.map +1 -1
  341. package/dist/web-images.d.ts +47 -0
  342. package/dist/web-images.js +169 -0
  343. package/dist/web-images.js.map +1 -0
  344. package/dist/web-mentions.d.ts +44 -0
  345. package/dist/web-mentions.js +75 -0
  346. package/dist/web-mentions.js.map +1 -0
  347. package/dist/web-prefs.d.ts +63 -0
  348. package/dist/web-prefs.js +265 -0
  349. package/dist/web-prefs.js.map +1 -0
  350. package/dist/web-preview.d.ts +14 -1
  351. package/dist/web-preview.js +35 -13
  352. package/dist/web-preview.js.map +1 -1
  353. package/dist/web-reply-files.d.ts +45 -0
  354. package/dist/web-reply-files.js +223 -0
  355. package/dist/web-reply-files.js.map +1 -0
  356. package/dist/web-schedules.d.ts +12 -0
  357. package/dist/web-schedules.js +34 -0
  358. package/dist/web-schedules.js.map +1 -0
  359. package/dist/web-server-drain.d.ts +10 -0
  360. package/dist/web-server-drain.js +50 -0
  361. package/dist/web-server-drain.js.map +1 -0
  362. package/dist/web-session.d.ts +36 -1
  363. package/dist/web-session.js +53 -6
  364. package/dist/web-session.js.map +1 -1
  365. package/dist/web-settings-patch.d.ts +23 -0
  366. package/dist/web-settings-patch.js +77 -0
  367. package/dist/web-settings-patch.js.map +1 -0
  368. package/dist/web-shell-routes.d.ts +5 -1
  369. package/dist/web-shell-routes.js +10 -1
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/dist/web-upload.d.ts +17 -4
  372. package/dist/web-upload.js +64 -13
  373. package/dist/web-upload.js.map +1 -1
  374. package/dist/workflow-templates/default.md +1 -1
  375. package/package.json +1 -1
  376. package/templates/launchd.plist.ejs +3 -1
  377. 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,12 @@ 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 { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
33
+ import { parseMentionQuery } from "./web-mentions.js";
27
34
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
28
35
  import { measureSyncWork } from "./sync-work-attribution.js";
36
+ import { resolveTz } from "./tz-utils.js";
29
37
  // ── Strict public-facing schemas ────────────────────────────────────────────
30
38
  // web-api endpoints must reject unknown fields so the dashboard cannot inject
31
39
  // internal-only flags that would reach handleCreate/scheduler/config writers.
@@ -45,18 +53,17 @@ const TaskUpdateSchema = z.object({
45
53
  description: z.string().max(MAX_TEXT).optional(),
46
54
  priority: z.string().max(64).optional(),
47
55
  }).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
56
  const TeamCreateSchema = z.object({
56
57
  name: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/),
57
58
  members: z.array(z.string().min(1).max(256)).min(1).max(256),
58
59
  description: z.string().max(MAX_TEXT).optional(),
59
60
  }).strict();
61
+ // #1689: an edit of a team — its members as they should be, and its description ("" clears it). No name: a team is
62
+ // not renamed.
63
+ const TeamEditSchema = z.object({
64
+ members: z.array(z.string().min(1).max(256)).min(1).max(256),
65
+ description: z.string().max(MAX_TEXT).optional(),
66
+ }).strict();
60
67
  const ConfigUpdateSchema = z.object({
61
68
  channel: z.object({
62
69
  group_id: z.union([z.number(), z.string()]).optional(),
@@ -130,6 +137,18 @@ function json(res, status, data) {
130
137
  res.writeHead(status);
131
138
  res.end(JSON.stringify(data));
132
139
  }
140
+ /**
141
+ * #1630 (#1635 review): an answer that waited on something (a list, a download) is sent only if the request is still
142
+ * allowed — the session not revoked meanwhile, a public link's exposure still current. Otherwise 401, and nothing.
143
+ */
144
+ function stillAuthorized(req, res, url, ctx) {
145
+ if (!permitWebContinuation(req, res, ctx))
146
+ return false;
147
+ if (isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false }))
148
+ return true;
149
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
150
+ return false;
151
+ }
133
152
  /**
134
153
  * Handle a Web UI request. Returns true if handled, false to pass through.
135
154
  */
@@ -328,6 +347,45 @@ export function handleWebRequest(req, res, url, ctx) {
328
347
  json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
329
348
  return true;
330
349
  }
350
+ // ── #1218: the Log tab ─────────────────────────────────
351
+ // An instance's steps, read on demand from its own transcript (log-service.ts). Read-only, and offered on the public
352
+ // link on purpose (the user's decision): the same masking applies to every viewer. Passive (web-auth.ts).
353
+ if (method === "GET" && path === "/ui/steps") {
354
+ const p = url.searchParams;
355
+ const instance = p.get("instance") ?? "";
356
+ const num = (k) => {
357
+ const v = p.get(k);
358
+ if (v === null)
359
+ return undefined;
360
+ const n = Number(v);
361
+ return /^\d{1,15}$/.test(v) && Number.isSafeInteger(n) ? n : null;
362
+ };
363
+ const after = num("after"), before = num("before"), ev = num("ev"), limit = num("limit");
364
+ const key = p.get("key");
365
+ if (!instance || instance.length > 128 || after === null || before === null || ev === null || limit === null
366
+ || (limit !== undefined && (limit < 1 || limit > 200)) || (key !== null && !/^\d{1,20}-\d{1,20}$/.test(key))) {
367
+ json(res, 400, { error: "instance (1-128 chars); after, before, ev: offsets; limit 1-200; key: <dev>-<ino>" });
368
+ return true;
369
+ }
370
+ if (!ctx.logSteps) {
371
+ json(res, 404, { error: "not available" });
372
+ return true;
373
+ }
374
+ ctx.logSteps(instance, {
375
+ ...(after !== undefined ? { after } : {}), ...(before !== undefined ? { before } : {}), ...(ev !== undefined ? { ev } : {}),
376
+ ...(limit !== undefined ? { limit } : {}), ...(key !== null ? { key } : {}),
377
+ skip: p.get("skip") === "1", watch: p.get("watch") === "1",
378
+ }).then(answer => {
379
+ if (answer === null)
380
+ json(res, 404, { error: `Instance not found: ${instance}` });
381
+ else
382
+ json(res, 200, answer);
383
+ }).catch(err => {
384
+ ctx.logger.error({ err: err.message, instance }, "Log steps read failed");
385
+ json(res, 500, { error: "could not read the steps" });
386
+ });
387
+ return true;
388
+ }
331
389
  // ── Send message ───────────────────────────────────────
332
390
  if (method === "POST" && path === "/ui/send") {
333
391
  handleSendMessage(req, res, ctx);
@@ -365,6 +423,66 @@ export function handleWebRequest(req, res, url, ctx) {
365
423
  res.end(bytes);
366
424
  return true;
367
425
  }
426
+ // ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
427
+ const imageMatch = path.match(/^\/ui\/img\/(bot|avatar|emoji)\/([^/]+)$/);
428
+ if (method === "GET" && imageMatch) {
429
+ const kind = imageMatch[1];
430
+ let key;
431
+ try {
432
+ key = decodeURIComponent(imageMatch[2]);
433
+ }
434
+ catch {
435
+ json(res, 404, { error: "No such image" });
436
+ return true;
437
+ }
438
+ const send = (image) => {
439
+ if (res.headersSent || res.destroyed)
440
+ return;
441
+ if (!stillAuthorized(req, res, url, ctx))
442
+ return;
443
+ if (!image) {
444
+ json(res, 404, { error: "No such image" });
445
+ return;
446
+ }
447
+ res.setHeader("Content-Type", image.mime);
448
+ res.setHeader("X-Content-Type-Options", "nosniff");
449
+ // A changed avatar is a new URL (?v=); an emoji id is one picture for good.
450
+ res.setHeader("Cache-Control", "private, max-age=86400");
451
+ res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
452
+ res.writeHead(200);
453
+ res.end(image.bytes);
454
+ };
455
+ if (kind === "emoji") {
456
+ (ctx.webEmoji ? ctx.webEmoji(key) : Promise.resolve(null)).then(send, () => send(null));
457
+ return true;
458
+ }
459
+ send(ctx.webImage?.(kind, key) ?? null);
460
+ return true;
461
+ }
462
+ if (method === "GET" && path === "/ui/emojis") {
463
+ const answer = (emojis) => { if (!res.headersSent && !res.destroyed && stillAuthorized(req, res, url, ctx))
464
+ json(res, 200, { emojis }); };
465
+ (ctx.webEmojiList ? ctx.webEmojiList() : Promise.resolve([])).then(answer, () => answer([]));
466
+ return true;
467
+ }
468
+ // #1682: the names of the mention ids a page found in what it rendered (at most MAX_MENTION_IDS, digits only) — only
469
+ // those, so no page (the public link included) ever gets a directory; from memory, never a request.
470
+ if (method === "GET" && path === "/ui/mentions") {
471
+ const parsed = parseMentionQuery(url.searchParams);
472
+ if (!parsed.ok) {
473
+ json(res, 400, { error: parsed.error });
474
+ return true;
475
+ }
476
+ let names = { u: {}, r: {}, c: {} };
477
+ try {
478
+ names = ctx.webMentionNames?.(parsed.query) ?? names;
479
+ }
480
+ catch (err) {
481
+ ctx.logger.debug({ err: err.message }, "mention names unavailable");
482
+ }
483
+ json(res, 200, names);
484
+ return true;
485
+ }
368
486
  // ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
369
487
  if (method === "GET" && path === "/ui/prompts") {
370
488
  json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
@@ -444,6 +562,97 @@ export function handleWebRequest(req, res, url, ctx) {
444
562
  });
445
563
  return true;
446
564
  }
565
+ // #1723: the public link page's preview, asked while it says "Preview is starting…" — its state and, once up, its
566
+ // origin and boot. Public link only: a local page's preview is decided by its own load.
567
+ if (path === "/ui/preview-status" && method === "GET") {
568
+ const gateway = gatewayRequestContext(req);
569
+ if (!gateway || !ctx.publicPreviewForUi) {
570
+ json(res, 404, { error: "not offered here" });
571
+ return true;
572
+ }
573
+ const p = ctx.publicPreviewForUi(gateway.exposureId, gateway.expectedOrigin);
574
+ json(res, 200, { code: p.code, previewOrigin: p.previewOrigin, boot: p.previewOrigin ? p.boot : null, detail: p.detail ?? null });
575
+ return true;
576
+ }
577
+ // #1665: the web preferences that follow the person between devices. Read by any page of the panel, the public link
578
+ // included (it reads what is in force); written only by a signed-in admin session or the CLI's header token — never
579
+ // over the public link (absent from its route manifest, and refused here too), never by a read-only session.
580
+ if (path === "/ui/prefs" && method === "GET") {
581
+ if (!ctx.webPrefs) {
582
+ json(res, 404, { error: "Preferences are not kept on this fleet" });
583
+ return true;
584
+ }
585
+ json(res, 200, ctx.webPrefs.read(OPERATOR));
586
+ return true;
587
+ }
588
+ if (path === "/ui/prefs" && method === "POST") {
589
+ const prefs = ctx.webPrefs;
590
+ if (!prefs) {
591
+ json(res, 404, { error: "Preferences are not kept on this fleet" });
592
+ return true;
593
+ }
594
+ if (gatewayRequestContext(req)) {
595
+ json(res, 403, { error: "prefs_not_over_public_link" });
596
+ return true;
597
+ }
598
+ // Who may write, asked now and again once the body is in: a session revoked or expired, or a token rotated, while
599
+ // the body was on its way writes nothing and announces nothing.
600
+ const writer = () => {
601
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
602
+ if (verdict.kind !== "allow")
603
+ return 401;
604
+ return verdict.via === "session" && verdict.session?.tier !== "admin" ? 403 : 200;
605
+ };
606
+ const refuse = (code) => json(res, code, code === 401 ? { error: WEB_TOKEN_INVALID_MESSAGE } : { error: "prefs_read_only" });
607
+ const first = writer();
608
+ if (first !== 200) {
609
+ refuse(first);
610
+ return true;
611
+ }
612
+ (async () => {
613
+ let body;
614
+ try {
615
+ body = await parseBody(req);
616
+ }
617
+ catch {
618
+ json(res, 400, { error: "Invalid JSON" });
619
+ return;
620
+ }
621
+ if (!permitWebContinuation(req, res, ctx))
622
+ return;
623
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
624
+ json(res, 400, { error: "set required" });
625
+ return;
626
+ }
627
+ const parsed = prefs.parsePatch(body.set);
628
+ if (!parsed.ok) {
629
+ json(res, 400, { error: parsed.error });
630
+ return;
631
+ }
632
+ const now = writer();
633
+ if (now !== 200) {
634
+ refuse(now);
635
+ return;
636
+ }
637
+ const onlyIfUnset = body.onlyIfUnset === true;
638
+ const written = prefs.write(OPERATOR, parsed.patch, { onlyIfUnset });
639
+ if (!written.ok) {
640
+ json(res, written.status, { error: written.error });
641
+ return;
642
+ }
643
+ // Another device sees the new rev in the next status frame — at once on the live stream.
644
+ if (written.changed.length)
645
+ ctx.emitSseEvent("status", ctx.getUiStatus());
646
+ json(res, 200, { ...written.view, changed: written.changed });
647
+ })().catch(err => {
648
+ ctx.logger.error({ err: err.message }, "Saving web preferences failed");
649
+ try {
650
+ json(res, 500, { error: "Saving preferences failed" });
651
+ }
652
+ catch { /* already answered */ }
653
+ });
654
+ return true;
655
+ }
447
656
  // #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
448
657
  // token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
449
658
  if (method === "POST" && path === "/ui/needs/ack") {
@@ -586,13 +795,18 @@ export function handleWebRequest(req, res, url, ctx) {
586
795
  }
587
796
  const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
588
797
  (async () => {
798
+ let held = false;
589
799
  try {
590
- await ctx.startInstance(name, config, topicMode ?? false);
800
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
591
801
  ctx.emitSseEvent("status", ctx.getUiStatus());
592
- json(res, 200, { started: name });
802
+ if (!held)
803
+ json(res, 200, { started: name });
593
804
  }
594
805
  catch (err) {
595
- json(res, 500, { error: err.message });
806
+ if (!held)
807
+ json(res, 500, { error: err.message });
808
+ else
809
+ ctx.logger.error({ err, name }, "Held instance action failed");
596
810
  }
597
811
  })();
598
812
  return true;
@@ -705,14 +919,19 @@ export function handleWebRequest(req, res, url, ctx) {
705
919
  if (method === "POST" && restartMatch) {
706
920
  const name = decodeURIComponent(restartMatch[1]);
707
921
  (async () => {
922
+ let held = false;
708
923
  try {
709
- await ctx.restartSingleInstance(name, { explicit: true });
924
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.restartSingleInstance(name, { explicit: true }));
710
925
  ctx.emitSseEvent("status", ctx.getUiStatus());
711
- json(res, 200, { restarted: name });
926
+ if (!held)
927
+ json(res, 200, { restarted: name });
712
928
  }
713
929
  catch (err) {
714
930
  const status = err.message.includes("not found") ? 404 : 500;
715
- json(res, status, { error: err.message });
931
+ if (!held)
932
+ json(res, status, { error: err.message });
933
+ else
934
+ ctx.logger.error({ err, name }, "Held instance action failed");
716
935
  }
717
936
  })();
718
937
  return true;
@@ -830,10 +1049,28 @@ export function handleWebRequest(req, res, url, ctx) {
830
1049
  json(res, 200, { schedules: [] });
831
1050
  return true;
832
1051
  }
833
- json(res, 200, { schedules: ctx.scheduler.list() });
1052
+ // #1724: each row with its next run and one-shot time in the fleet's zone — worked out here, not on the page.
1053
+ const scheduler = ctx.scheduler;
1054
+ const now = Date.now();
1055
+ json(res, 200, {
1056
+ schedules: scheduler.list().map((s) => scheduleView(s, scheduler.getRetry?.(s.id) ?? null, now)),
1057
+ // The zone a new schedule (and a "Once" date and time typed without one) is read in.
1058
+ default_timezone: ctx.fleetConfig?.defaults?.scheduler?.default_timezone
1059
+ ?? DEFAULT_SCHEDULER_CONFIG.default_timezone,
1060
+ });
834
1061
  return true;
835
1062
  }
1063
+ // #1724: who may write a schedule, asked again once the body is in (a session revoked meanwhile, a non-admin tier).
1064
+ const scheduleWriterGone = () => {
1065
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1066
+ return verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin");
1067
+ };
836
1068
  if (method === "POST" && path === "/ui/schedules") {
1069
+ // #1724: schedules are not created, edited or deleted over the public link (absent from its manifest too, #1689).
1070
+ if (gatewayRequestContext(req)) {
1071
+ json(res, 403, { error: "schedule_not_over_public_link" });
1072
+ return true;
1073
+ }
837
1074
  if (!ctx.scheduler) {
838
1075
  json(res, 500, { error: "Scheduler not initialized" });
839
1076
  return true;
@@ -843,10 +1080,23 @@ export function handleWebRequest(req, res, url, ctx) {
843
1080
  const body = await parseBody(req);
844
1081
  if (!permitWebContinuation(req, res, ctx))
845
1082
  return;
846
- const parsed = parseOrReject(ScheduleCreateSchema, body, res);
1083
+ if (scheduleWriterGone()) {
1084
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1085
+ return;
1086
+ }
1087
+ // #1724: the create_schedule tool's own schema — cron or at (exactly one) — plus a target, which the web names.
1088
+ const parsed = parseOrReject(CreateScheduleArgs, body, res);
847
1089
  if (!parsed)
848
1090
  return;
849
- const schedule = ctx.scheduler.create(parsed);
1091
+ if (typeof parsed.target !== "string" || !parsed.target) {
1092
+ json(res, 400, { error: "target: an instance is required" });
1093
+ return;
1094
+ }
1095
+ const schedule = ctx.scheduler.create({
1096
+ cron: parsed.cron, at: parsed.at, message: parsed.message, target: parsed.target, label: parsed.label, timezone: parsed.timezone,
1097
+ ...(parsed.silent !== undefined ? { silent: parsed.silent } : {}),
1098
+ source: "web", reply_chat_id: "", reply_thread_id: null,
1099
+ });
850
1100
  json(res, 200, schedule);
851
1101
  }
852
1102
  catch (err) {
@@ -856,7 +1106,48 @@ export function handleWebRequest(req, res, url, ctx) {
856
1106
  return true;
857
1107
  }
858
1108
  const schedDelMatch = path.match(/^\/ui\/schedules\/(.+)$/);
1109
+ // #1724: Edit — the update_schedule tool's validation (UpdateScheduleArgs) and its call (scheduler.update): cron and at
1110
+ // clear each other there; silent is not touched.
1111
+ if (method === "PATCH" && schedDelMatch) {
1112
+ if (gatewayRequestContext(req)) {
1113
+ json(res, 403, { error: "schedule_not_over_public_link" });
1114
+ return true;
1115
+ }
1116
+ if (!ctx.scheduler || typeof ctx.scheduler.update !== "function") {
1117
+ json(res, 500, { error: "Scheduler not initialized" });
1118
+ return true;
1119
+ }
1120
+ const id = decodeURIComponent(schedDelMatch[1]);
1121
+ (async () => {
1122
+ try {
1123
+ const body = await parseBody(req);
1124
+ if (!permitWebContinuation(req, res, ctx))
1125
+ return;
1126
+ if (scheduleWriterGone()) {
1127
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1128
+ return;
1129
+ }
1130
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.hasOwn(body, "id")) {
1131
+ json(res, 400, { error: "expected the fields to change (the id is the path's)" });
1132
+ return;
1133
+ }
1134
+ const parsed = parseOrReject(UpdateScheduleArgs, { ...body, id }, res);
1135
+ if (!parsed)
1136
+ return;
1137
+ const { id: _id, ...changes } = parsed;
1138
+ json(res, 200, ctx.scheduler.update(id, changes));
1139
+ }
1140
+ catch (err) {
1141
+ json(res, 400, { error: err.message });
1142
+ }
1143
+ })();
1144
+ return true;
1145
+ }
859
1146
  if (method === "DELETE" && schedDelMatch) {
1147
+ if (gatewayRequestContext(req)) {
1148
+ json(res, 403, { error: "schedule_not_over_public_link" });
1149
+ return true;
1150
+ }
860
1151
  if (!ctx.scheduler) {
861
1152
  json(res, 500, { error: "Scheduler not initialized" });
862
1153
  return true;
@@ -893,26 +1184,82 @@ export function handleWebRequest(req, res, url, ctx) {
893
1184
  return true;
894
1185
  }
895
1186
  if (method === "POST" && path === "/ui/teams") {
1187
+ // #1689: not over the public link (absent from its manifest too), like editing and deleting a team.
1188
+ if (gatewayRequestContext(req)) {
1189
+ json(res, 403, { error: "teams_not_over_public_link" });
1190
+ return true;
1191
+ }
896
1192
  (async () => {
897
1193
  try {
898
1194
  const body = await parseBody(req);
899
1195
  if (!permitWebContinuation(req, res, ctx))
900
1196
  return;
1197
+ // #1692: who may write, asked again once the body is in (as the edit does).
1198
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1199
+ if (verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin")) {
1200
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1201
+ return;
1202
+ }
901
1203
  const parsed = parseOrReject(TeamCreateSchema, body, res);
902
1204
  if (!parsed)
903
1205
  return;
904
- if (!ctx.fleetConfig) {
905
- json(res, 500, { error: "No fleet config" });
1206
+ // #1692: the create_team path — members checked, an existing team never overwritten, a failed save undone.
1207
+ const r = commitTeamCreate(ctx.fleetConfig, parsed, () => ctx.saveFleetConfig());
1208
+ if (!r.ok) {
1209
+ json(res, r.status, { error: r.error });
906
1210
  return;
907
1211
  }
908
- if (!ctx.fleetConfig.teams)
909
- ctx.fleetConfig.teams = {};
910
- ctx.fleetConfig.teams[parsed.name] = {
911
- members: parsed.members,
912
- description: parsed.description,
913
- };
914
- ctx.saveFleetConfig();
915
- json(res, 200, { created: parsed.name });
1212
+ json(res, 200, r.result);
1213
+ }
1214
+ catch (err) {
1215
+ json(res, 400, { error: err.message });
1216
+ }
1217
+ })();
1218
+ return true;
1219
+ }
1220
+ // #1689: edit a team — the same update as the update_team tool (applyTeamUpdate), from the members the page sends:
1221
+ // what to add and remove is worked out against the team as it is now. Never over the public link.
1222
+ const teamEditMatch = path.match(/^\/ui\/teams\/(.+)$/);
1223
+ if ((method === "PATCH" || method === "PUT") && teamEditMatch) {
1224
+ if (gatewayRequestContext(req)) {
1225
+ json(res, 403, { error: "teams_not_over_public_link" });
1226
+ return true;
1227
+ }
1228
+ const name = decodeURIComponent(teamEditMatch[1]);
1229
+ // Who may write, asked again once the body is in (#1693 review): a session revoked or a token rotated meanwhile
1230
+ // writes nothing.
1231
+ const writerNow = () => {
1232
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1233
+ return verdict.kind === "allow" && !(verdict.via === "session" && verdict.session?.tier !== "admin");
1234
+ };
1235
+ (async () => {
1236
+ try {
1237
+ const body = await parseBody(req);
1238
+ if (!permitWebContinuation(req, res, ctx))
1239
+ return;
1240
+ if (!writerNow()) {
1241
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1242
+ return;
1243
+ }
1244
+ const parsed = parseOrReject(TeamEditSchema, body, res);
1245
+ if (!parsed)
1246
+ return;
1247
+ const teams = ctx.fleetConfig?.teams;
1248
+ const current = teams && Object.hasOwn(teams, name) ? teams[name].members : null;
1249
+ if (!current) {
1250
+ json(res, 404, { error: `Team not found: ${name}` });
1251
+ return;
1252
+ }
1253
+ const want = [...new Set(parsed.members)];
1254
+ // The update and its save as one: a failed save leaves the team as it was (500).
1255
+ const r = commitTeamUpdate(ctx.fleetConfig, {
1256
+ name, add: want.filter(m => !current.includes(m)), remove: current.filter(m => !want.includes(m)), description: parsed.description,
1257
+ }, () => ctx.saveFleetConfig());
1258
+ if (!r.ok) {
1259
+ json(res, r.status, { error: r.error });
1260
+ return;
1261
+ }
1262
+ json(res, 200, { updated: r.result });
916
1263
  }
917
1264
  catch (err) {
918
1265
  json(res, 400, { error: err.message });
@@ -922,8 +1269,13 @@ export function handleWebRequest(req, res, url, ctx) {
922
1269
  }
923
1270
  const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
924
1271
  if (method === "DELETE" && teamDelMatch) {
1272
+ if (gatewayRequestContext(req)) {
1273
+ json(res, 403, { error: "teams_not_over_public_link" });
1274
+ return true;
1275
+ }
925
1276
  const name = decodeURIComponent(teamDelMatch[1]);
926
- if (!ctx.fleetConfig?.teams?.[name]) {
1277
+ // Own keys (#1689): a team is one fleet.yaml defines — "__proto__" or "constructor" is not found.
1278
+ if (!ctx.fleetConfig?.teams || !Object.hasOwn(ctx.fleetConfig.teams, name)) {
927
1279
  json(res, 404, { error: `Team not found: ${name}` });
928
1280
  return true;
929
1281
  }
@@ -1092,7 +1444,10 @@ function handleSendMessage(req, res, ctx) {
1092
1444
  }) : undefined;
1093
1445
  let delivered;
1094
1446
  try {
1095
- delivered = await ctx.deliverToInstance(instance, {
1447
+ const deliver = (name, payload) => ctx.deliverHumanToInstance
1448
+ ? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
1449
+ : ctx.deliverToInstance(name, payload);
1450
+ delivered = await deliver(instance, {
1096
1451
  type: "fleet_inbound",
1097
1452
  content: delivery.text,
1098
1453
  targetSession: instance,
@@ -1129,6 +1484,13 @@ function handleSendMessage(req, res, ctx) {
1129
1484
  }
1130
1485
  settleEcho?.(true);
1131
1486
  settleClassicEcho?.(true);
1487
+ // #1666: the same cancel/progress bubble a platform inbound starts, in the topic the echo went to — a second turn
1488
+ // on the echo's lane, so the topic reads "echo, then bubble" and a cancel in between drops it (the lane's epoch).
1489
+ // Not a copy (#1668): a slow bubble never drops the next message's echo, and a slow echo never drops the bubble.
1490
+ if (settleEcho && ctx.startWebChatProgress) {
1491
+ const startProgress = ctx.startWebChatProgress.bind(ctx);
1492
+ ctx.reserveWebChannelEcho?.(instance, () => startProgress(instance), { copy: false })(true);
1493
+ }
1132
1494
  ctx.webFiles?.commit(files);
1133
1495
  ctx.lastInboundUser.set(instance, "web-user");
1134
1496
  ctx.eventLog?.logActivity("message", "web-user", (message || `[${files.length} file(s)]`).slice(0, 200), instance);
@@ -1209,8 +1571,12 @@ function handleUpload(req, res, url, ctx) {
1209
1571
  });
1210
1572
  }
1211
1573
  function shellPreview(req, ctx, mode) {
1212
- if (mode !== "full" || gatewayRequestContext(req))
1574
+ if (mode !== "full")
1213
1575
  return null;
1576
+ // #1723: a page of the public link asks its own exposure's preview — never the local listener's Host-based choice.
1577
+ const gateway = gatewayRequestContext(req);
1578
+ if (gateway)
1579
+ return ctx.publicPreviewForUi?.(gateway.exposureId, gateway.expectedOrigin) ?? null;
1214
1580
  return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
1215
1581
  }
1216
1582
  /**
@@ -1259,6 +1625,21 @@ function serveMediaRange(req, res, files, id, file) {
1259
1625
  res.writeHead(ranged ? 206 : 200);
1260
1626
  res.end(got.bytes);
1261
1627
  }
1628
+ /**
1629
+ * #1665: `<html>` carrying the host's preferences (data-prefs: `{"rev":n,"values":{…}}`, attribute-escaped). theme.js (a
1630
+ * blocking script in <head>) and the app's dictionary take the theme and language from it over the browser's cache,
1631
+ * and app-prefs.js copies every value into that cache before any panel reads it — so no device paints a stale theme
1632
+ * or replays a tour already seen, a new device included. Only for the signed-in app (the public link reads what is in
1633
+ * force too); the anonymous View reader and the sign-in page keep the browser's own.
1634
+ */
1635
+ function shellHtmlTag(ctx, mode) {
1636
+ if (mode !== "full" || !ctx.webPrefs)
1637
+ return '<html lang="en">';
1638
+ const view = ctx.webPrefs.read(OPERATOR);
1639
+ const lang = view.values.lang === "zh-TW" ? "zh-Hant" : "en";
1640
+ const attr = (v) => v.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
1641
+ return `<html lang="${lang}" data-prefs="${attr(JSON.stringify({ rev: view.rev, values: view.values }))}">`;
1642
+ }
1262
1643
  function shellBodyTag(req, ctx, mode) {
1263
1644
  const p = shellPreview(req, ctx, mode);
1264
1645
  const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
@@ -1266,8 +1647,14 @@ function shellBodyTag(req, ctx, mode) {
1266
1647
  + ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
1267
1648
  // #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
1268
1649
  + ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
1650
+ // #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
1651
+ + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
1269
1652
  // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
1270
1653
  + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
1654
+ // #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
1655
+ + ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
1656
+ // #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
1657
+ + ` data-fleet-tz="${attr(resolveTz())}"`
1271
1658
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
1272
1659
  + `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}>`;
1273
1660
  }
@@ -1282,7 +1669,10 @@ export function serveAppShell(req, res, ctx, mode) {
1282
1669
  const own = mode === "full" && !gatewayRequestContext(req)
1283
1670
  ? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) : null;
1284
1671
  const mediaSrc = own ? `${own}/ui/file/` : "'none'";
1285
- sendPanelHtml(res, html.replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(p?.previewOrigin ? { frameSrc: `${p.previewOrigin}/frame` } : {}), mediaSrc });
1672
+ // A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
1673
+ // the page may load); every other page frames exactly the origin this load chose (#1723).
1674
+ const frameSrc = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
1675
+ sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode)).replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
1286
1676
  }
1287
1677
  catch {
1288
1678
  json(res, 500, { error: "app.html not found" });