@songsid/agend 2.3.3-beta.1 → 2.3.3-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 (355) hide show
  1. package/dist/agent-cli.js +13 -112
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +0 -1
  4. package/dist/agent-endpoint.js +1 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +3 -6
  7. package/dist/agent-whats-new.js +7 -18
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.d.ts +0 -7
  10. package/dist/auth-api.js +1 -15
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.js +1 -1
  13. package/dist/authority/authority-state.js.map +1 -1
  14. package/dist/authority/web-authority.d.ts +9 -4
  15. package/dist/authority/web-authority.js +10 -5
  16. package/dist/authority/web-authority.js.map +1 -1
  17. package/dist/backend/antigravity.js +6 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +7 -0
  20. package/dist/backend/claude-code.js +34 -28
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  23. package/dist/backend/claude-transcript-errors.js +19 -0
  24. package/dist/backend/claude-transcript-errors.js.map +1 -0
  25. package/dist/backend/codex-session-lookup.d.ts +16 -3
  26. package/dist/backend/codex-session-lookup.js +45 -11
  27. package/dist/backend/codex-session-lookup.js.map +1 -1
  28. package/dist/backend/codex.d.ts +16 -0
  29. package/dist/backend/codex.js +184 -35
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/grok.js +58 -12
  32. package/dist/backend/grok.js.map +1 -1
  33. package/dist/backend/kiro.js +2 -0
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/muse.js +2 -1
  36. package/dist/backend/muse.js.map +1 -1
  37. package/dist/backend/types.d.ts +46 -0
  38. package/dist/backend/types.js.map +1 -1
  39. package/dist/bundled-skills.d.ts +4 -6
  40. package/dist/bundled-skills.js +10 -14
  41. package/dist/bundled-skills.js.map +1 -1
  42. package/dist/channel/adapters/discord.js +60 -17
  43. package/dist/channel/adapters/discord.js.map +1 -1
  44. package/dist/channel/mcp-server.js +5 -5
  45. package/dist/channel/mcp-server.js.map +1 -1
  46. package/dist/channel/mcp-tools.d.ts +1 -3
  47. package/dist/channel/mcp-tools.js +4 -58
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/classic-channel-manager.d.ts +4 -1
  50. package/dist/classic-channel-manager.js +21 -7
  51. package/dist/classic-channel-manager.js.map +1 -1
  52. package/dist/cli.js +21 -4
  53. package/dist/cli.js.map +1 -1
  54. package/dist/config-history.d.ts +50 -0
  55. package/dist/config-history.js +263 -0
  56. package/dist/config-history.js.map +1 -0
  57. package/dist/config-validator.js +7 -7
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +3 -0
  60. package/dist/config.js +11 -4
  61. package/dist/config.js.map +1 -1
  62. package/dist/daemon.d.ts +73 -3
  63. package/dist/daemon.js +334 -121
  64. package/dist/daemon.js.map +1 -1
  65. package/dist/daily-summary.d.ts +8 -1
  66. package/dist/daily-summary.js +8 -5
  67. package/dist/daily-summary.js.map +1 -1
  68. package/dist/dashboard-live.d.ts +1 -1
  69. package/dist/dashboard-live.js +3 -3
  70. package/dist/dashboard-live.js.map +1 -1
  71. package/dist/dashboard-permissions.d.ts +1 -1
  72. package/dist/dashboard-permissions.js +1 -3
  73. package/dist/dashboard-permissions.js.map +1 -1
  74. package/dist/delivery-outbox.d.ts +4 -0
  75. package/dist/delivery-outbox.js +12 -2
  76. package/dist/delivery-outbox.js.map +1 -1
  77. package/dist/doctor.d.ts +4 -0
  78. package/dist/doctor.js +16 -1
  79. package/dist/doctor.js.map +1 -1
  80. package/dist/fleet-level-config.js +1 -1
  81. package/dist/fleet-level-config.js.map +1 -1
  82. package/dist/fleet-manager.d.ts +70 -54
  83. package/dist/fleet-manager.js +257 -322
  84. package/dist/fleet-manager.js.map +1 -1
  85. package/dist/fleet-process-handlers.d.ts +5 -0
  86. package/dist/fleet-process-handlers.js +21 -0
  87. package/dist/fleet-process-handlers.js.map +1 -1
  88. package/dist/fleet-service-owner.d.ts +11 -0
  89. package/dist/fleet-service-owner.js +3 -0
  90. package/dist/fleet-service-owner.js.map +1 -0
  91. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  92. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
  93. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  94. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +29 -67
  95. package/dist/instance-lifecycle.d.ts +19 -0
  96. package/dist/instance-lifecycle.js +52 -10
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/locale.js +92 -42
  99. package/dist/locale.js.map +1 -1
  100. package/dist/needs-you-hub.d.ts +1 -0
  101. package/dist/needs-you-hub.js +6 -3
  102. package/dist/needs-you-hub.js.map +1 -1
  103. package/dist/needs-you-live.d.ts +3 -1
  104. package/dist/needs-you-live.js +24 -2
  105. package/dist/needs-you-live.js.map +1 -1
  106. package/dist/needs-you.d.ts +13 -2
  107. package/dist/needs-you.js +11 -1
  108. package/dist/needs-you.js.map +1 -1
  109. package/dist/notify-socket.d.ts +10 -0
  110. package/dist/notify-socket.js +45 -0
  111. package/dist/notify-socket.js.map +1 -0
  112. package/dist/outbound-handlers.js +19 -7
  113. package/dist/outbound-handlers.js.map +1 -1
  114. package/dist/outbound-schemas.d.ts +0 -62
  115. package/dist/outbound-schemas.js +3 -42
  116. package/dist/outbound-schemas.js.map +1 -1
  117. package/dist/parked-notices.d.ts +87 -0
  118. package/dist/parked-notices.js +194 -0
  119. package/dist/parked-notices.js.map +1 -0
  120. package/dist/public-link-progress.d.ts +5 -2
  121. package/dist/public-link-progress.js +4 -3
  122. package/dist/public-link-progress.js.map +1 -1
  123. package/dist/public-web-gateway.js +0 -9
  124. package/dist/public-web-gateway.js.map +1 -1
  125. package/dist/public-web-link.d.ts +12 -0
  126. package/dist/public-web-link.js +29 -5
  127. package/dist/public-web-link.js.map +1 -1
  128. package/dist/quota-notice.d.ts +23 -0
  129. package/dist/quota-notice.js +22 -0
  130. package/dist/quota-notice.js.map +1 -0
  131. package/dist/scheduler/task-board.d.ts +0 -13
  132. package/dist/scheduler/task-board.js +0 -14
  133. package/dist/scheduler/task-board.js.map +1 -1
  134. package/dist/sd-notify.d.ts +10 -1
  135. package/dist/sd-notify.js +27 -2
  136. package/dist/sd-notify.js.map +1 -1
  137. package/dist/search/search-docs.d.ts +1 -1
  138. package/dist/search/search-docs.js +2 -2
  139. package/dist/search/search-docs.js.map +1 -1
  140. package/dist/settings-api.d.ts +16 -0
  141. package/dist/settings-api.js +55 -2
  142. package/dist/settings-api.js.map +1 -1
  143. package/dist/settings-change.js +3 -1
  144. package/dist/settings-change.js.map +1 -1
  145. package/dist/settings-confirmation.d.ts +17 -3
  146. package/dist/settings-confirmation.js +8 -5
  147. package/dist/settings-confirmation.js.map +1 -1
  148. package/dist/settings-http-confirmation.js +31 -1
  149. package/dist/settings-http-confirmation.js.map +1 -1
  150. package/dist/setup-wizard.d.ts +3 -0
  151. package/dist/setup-wizard.js +4 -2
  152. package/dist/setup-wizard.js.map +1 -1
  153. package/dist/task-board-waker.d.ts +4 -1
  154. package/dist/task-board-waker.js +13 -2
  155. package/dist/task-board-waker.js.map +1 -1
  156. package/dist/tool-permissions.js +3 -10
  157. package/dist/tool-permissions.js.map +1 -1
  158. package/dist/topic-commands.d.ts +27 -7
  159. package/dist/topic-commands.js +95 -36
  160. package/dist/topic-commands.js.map +1 -1
  161. package/dist/transcript-monitor.d.ts +7 -0
  162. package/dist/transcript-monitor.js +48 -3
  163. package/dist/transcript-monitor.js.map +1 -1
  164. package/dist/tunnel/cloudflared.d.ts +5 -0
  165. package/dist/tunnel/cloudflared.js +32 -1
  166. package/dist/tunnel/cloudflared.js.map +1 -1
  167. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  168. package/dist/tunnel/named-diagnostics.js +4 -1
  169. package/dist/tunnel/named-diagnostics.js.map +1 -1
  170. package/dist/tunnel/named-link-test.d.ts +90 -0
  171. package/dist/tunnel/named-link-test.js +270 -0
  172. package/dist/tunnel/named-link-test.js.map +1 -0
  173. package/dist/tunnel/purpose-lane.d.ts +3 -1
  174. package/dist/tunnel/purpose-lane.js +2 -0
  175. package/dist/tunnel/purpose-lane.js.map +1 -1
  176. package/dist/types.d.ts +8 -6
  177. package/dist/ui/chat-ambient.js +4 -4
  178. package/dist/ui/chat-commands.js +3 -2
  179. package/dist/ui/chat-store.js +4 -1
  180. package/dist/ui/chat-strings.js +12 -46
  181. package/dist/ui/chat-thread.js +51 -59
  182. package/dist/ui/fleet-office-skin.js +37 -14
  183. package/dist/ui/fleet-office.js +31 -7
  184. package/dist/ui/html-attachment.js +22 -8
  185. package/dist/ui/panel-chat.js +21 -50
  186. package/dist/ui/panel-needs.js +11 -9
  187. package/dist/ui/panel-settings.js +5 -3
  188. package/dist/ui/preview-panel.js +12 -21
  189. package/dist/ui/preview.js +10 -3
  190. package/dist/ui/search-dialog.js +30 -14
  191. package/dist/ui/settings-appearance.js +1 -3
  192. package/dist/ui/settings-confirm.js +18 -0
  193. package/dist/ui/settings-guests.js +13 -4
  194. package/dist/ui/settings-learn.js +1 -1
  195. package/dist/ui/settings-mods.js +5 -12
  196. package/dist/ui/settings-named-test.js +144 -0
  197. package/dist/ui/settings-registry.js +0 -1
  198. package/dist/ui/settings-strings.js +14 -14
  199. package/dist/ui/settings-web-authority.js +4 -3
  200. package/dist/ui/shared/app-i18n.js +15 -4
  201. package/dist/ui/shared/app-needs.js +5 -1
  202. package/dist/ui/shared/app-prefs.js +17 -17
  203. package/dist/ui/shared/app-shell.js +47 -9
  204. package/dist/ui/shared/app-store.js +6 -2
  205. package/dist/ui/shared/app.css +33 -39
  206. package/dist/ui/shared/app.js +5 -4
  207. package/dist/ui/shared/instance-nav.js +9 -9
  208. package/dist/ui/shared/instance-switch.js +8 -2
  209. package/dist/ui/shared/panel-view.js +22 -8
  210. package/dist/ui/shared/view-strings.js +6 -0
  211. package/dist/ui/shared/web-authorize.js +35 -13
  212. package/dist/usage/usage-api.d.ts +3 -0
  213. package/dist/usage/usage-api.js +5 -1
  214. package/dist/usage/usage-api.js.map +1 -1
  215. package/dist/view-api.d.ts +2 -0
  216. package/dist/view-api.js +64 -0
  217. package/dist/view-api.js.map +1 -1
  218. package/dist/web-api.d.ts +9 -17
  219. package/dist/web-api.js +257 -371
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-appearance-tools.d.ts +0 -2
  222. package/dist/web-appearance-tools.js +2 -5
  223. package/dist/web-appearance-tools.js.map +1 -1
  224. package/dist/web-appearance-view.d.ts +0 -6
  225. package/dist/web-appearance-view.js +0 -1
  226. package/dist/web-appearance-view.js.map +1 -1
  227. package/dist/web-appearance.d.ts +14 -7
  228. package/dist/web-appearance.js +5 -6
  229. package/dist/web-appearance.js.map +1 -1
  230. package/dist/web-asset-parts.d.ts +1 -17
  231. package/dist/web-asset-parts.js +1 -15
  232. package/dist/web-asset-parts.js.map +1 -1
  233. package/dist/web-auth.js +1 -7
  234. package/dist/web-auth.js.map +1 -1
  235. package/dist/web-chat-history.d.ts +3 -7
  236. package/dist/web-chat-history.js +10 -17
  237. package/dist/web-chat-history.js.map +1 -1
  238. package/dist/web-commands.d.ts +9 -1
  239. package/dist/web-commands.js +13 -0
  240. package/dist/web-commands.js.map +1 -1
  241. package/dist/web-experimental.d.ts +2 -11
  242. package/dist/web-experimental.js +8 -36
  243. package/dist/web-experimental.js.map +1 -1
  244. package/dist/web-guest-access.d.ts +40 -2
  245. package/dist/web-guest-access.js +50 -14
  246. package/dist/web-guest-access.js.map +1 -1
  247. package/dist/web-mod-bundle.d.ts +2 -45
  248. package/dist/web-mod-bundle.js +15 -63
  249. package/dist/web-mod-bundle.js.map +1 -1
  250. package/dist/web-mod-store.d.ts +3 -48
  251. package/dist/web-mod-store.js +30 -97
  252. package/dist/web-mod-store.js.map +1 -1
  253. package/dist/web-prefs.d.ts +13 -0
  254. package/dist/web-prefs.js +38 -0
  255. package/dist/web-prefs.js.map +1 -1
  256. package/dist/web-preview.d.ts +6 -13
  257. package/dist/web-preview.js +12 -23
  258. package/dist/web-preview.js.map +1 -1
  259. package/dist/web-session.d.ts +11 -3
  260. package/dist/web-session.js +22 -11
  261. package/dist/web-session.js.map +1 -1
  262. package/dist/web-settings-patch.d.ts +1 -0
  263. package/dist/web-settings-patch.js +6 -3
  264. package/dist/web-settings-patch.js.map +1 -1
  265. package/dist/web-settings-tools.d.ts +0 -13
  266. package/dist/web-settings-tools.js +12 -84
  267. package/dist/web-settings-tools.js.map +1 -1
  268. package/launcher/agend-install.cjs +7 -1
  269. package/launcher/fleet-service-owner.cjs +150 -0
  270. package/package.json +1 -1
  271. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  272. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
  273. package/dist/task-board-summary.d.ts +0 -101
  274. package/dist/task-board-summary.js +0 -177
  275. package/dist/task-board-summary.js.map +0 -1
  276. package/dist/ui/app-effects.js +0 -251
  277. package/dist/ui/chat-effect-posters.js +0 -42
  278. package/dist/ui/chat-effects.js +0 -167
  279. package/dist/ui/effect-trial-controller.js +0 -124
  280. package/dist/ui/effect-trial-offer.js +0 -2
  281. package/dist/ui/effect-trial-panel.js +0 -40
  282. package/dist/ui/effects-waker.js +0 -2
  283. package/dist/ui/settings-effects.js +0 -202
  284. package/dist/ui/settings-owner-effects.js +0 -82
  285. package/dist/ui/shared/effect-app-runtime.js +0 -299
  286. package/dist/ui/shared/effect-baseline.js +0 -64
  287. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  288. package/dist/ui/shared/effect-cursor.js +0 -126
  289. package/dist/ui/shared/effect-display-metadata.js +0 -39
  290. package/dist/ui/shared/effect-frame.js +0 -146
  291. package/dist/ui/shared/effect-geometry.js +0 -137
  292. package/dist/ui/shared/effect-host-style.js +0 -10
  293. package/dist/ui/shared/effect-owner-regions.js +0 -74
  294. package/dist/ui/shared/effect-owner-source.js +0 -122
  295. package/dist/ui/shared/effect-policy.js +0 -151
  296. package/dist/ui/shared/effect-poster-reader.js +0 -98
  297. package/dist/ui/shared/effect-posters.js +0 -117
  298. package/dist/ui/shared/effect-profiler.js +0 -184
  299. package/dist/ui/shared/effect-profiles.js +0 -65
  300. package/dist/ui/shared/effect-registry.js +0 -545
  301. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  302. package/dist/ui/shared/effect-safety.js +0 -153
  303. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  304. package/dist/ui/shared/effect-stock-trust.js +0 -217
  305. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  306. package/dist/ui/shared/effect-trial-source.js +0 -103
  307. package/dist/ui/shared/effect-trial-stage.js +0 -170
  308. package/dist/ui/shared/effect-trial.css +0 -40
  309. package/dist/ui/shared/feature-flags.js +0 -127
  310. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  311. package/dist/web-effect-bootstrap.d.ts +0 -12
  312. package/dist/web-effect-bootstrap.js +0 -49
  313. package/dist/web-effect-bootstrap.js.map +0 -1
  314. package/dist/web-effect-bundle.d.ts +0 -28
  315. package/dist/web-effect-bundle.js +0 -171
  316. package/dist/web-effect-bundle.js.map +0 -1
  317. package/dist/web-effect-canonical.d.ts +0 -8
  318. package/dist/web-effect-canonical.js +0 -27
  319. package/dist/web-effect-canonical.js.map +0 -1
  320. package/dist/web-effect-library.d.ts +0 -114
  321. package/dist/web-effect-library.js +0 -530
  322. package/dist/web-effect-library.js.map +0 -1
  323. package/dist/web-effect-playback.d.ts +0 -30
  324. package/dist/web-effect-playback.js +0 -71
  325. package/dist/web-effect-playback.js.map +0 -1
  326. package/dist/web-effect-poster.d.ts +0 -8
  327. package/dist/web-effect-poster.js +0 -52
  328. package/dist/web-effect-poster.js.map +0 -1
  329. package/dist/web-effect-profiles.d.ts +0 -118
  330. package/dist/web-effect-profiles.js +0 -181
  331. package/dist/web-effect-profiles.js.map +0 -1
  332. package/dist/web-effect-service.d.ts +0 -93
  333. package/dist/web-effect-service.js +0 -335
  334. package/dist/web-effect-service.js.map +0 -1
  335. package/dist/web-effect-tools.d.ts +0 -48
  336. package/dist/web-effect-tools.js +0 -129
  337. package/dist/web-effect-tools.js.map +0 -1
  338. package/dist/web-effect-trials.d.ts +0 -86
  339. package/dist/web-effect-trials.js +0 -204
  340. package/dist/web-effect-trials.js.map +0 -1
  341. package/dist/web-effect-upload.d.ts +0 -27
  342. package/dist/web-effect-upload.js +0 -185
  343. package/dist/web-effect-upload.js.map +0 -1
  344. package/dist/web-effect-worker.d.ts +0 -8
  345. package/dist/web-effect-worker.js +0 -170
  346. package/dist/web-effect-worker.js.map +0 -1
  347. package/dist/web-effect.d.ts +0 -103
  348. package/dist/web-effect.js +0 -153
  349. package/dist/web-effect.js.map +0 -1
  350. package/dist/web-experimental-http.d.ts +0 -2
  351. package/dist/web-experimental-http.js +0 -12
  352. package/dist/web-experimental-http.js.map +0 -1
  353. package/dist/web-mod-agent.d.ts +0 -50
  354. package/dist/web-mod-agent.js +0 -207
  355. package/dist/web-mod-agent.js.map +0 -1
package/dist/web-api.js CHANGED
@@ -1,8 +1,7 @@
1
- import { canvasWorkerEnabled, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
2
- import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
1
+ import { CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
3
2
  import { webHumanOrigin } from "./human-delivery-receipts.js";
4
3
  import { classifyWebPeer, confirmAfterBody, noAuthority } from "./authority/web-authority.js";
5
- import { guestChatRefusal, guestCommandText, guestDeliveries, guestFrameData, guestHistory, guestMessages, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
4
+ import { guestChatRefusal, guestCommandText, guestDeliveries, GUEST_PREF_MAX_BYTES, GUEST_SEARCHES, guestFrameData, guestHistory, guestMessages, guestOwnPrefs, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
6
5
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
7
6
  import { binaryProbe } from "./binary-probe.js";
8
7
  import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
@@ -14,7 +13,6 @@ import { buildModBundle } from "./web-mod-export.js";
14
13
  import { AGENT_WIDGET_SLOTS, checkWidgetData, checkWidgetSpec } from "./web-widget.js";
15
14
  import { checkOwnerCss, ownerCssAssetRefs } from "./web-owner-css.js";
16
15
  import { canonicalText } from "./web-theme.js";
17
- import { EFFECT_WIRE_LIMIT } from "./web-effect-upload.js";
18
16
  import { SKIN_WIRE_BYTES } from "./web-office-skins.js";
19
17
  import { gatewayRequestContext } from "./web-request-context.js";
20
18
  import { crossInstanceVisibility } from "./cross-instance-notice.js";
@@ -51,7 +49,6 @@ import { parseMentionQuery } from "./web-mentions.js";
51
49
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
52
50
  import { measureSyncWork } from "./sync-work-attribution.js";
53
51
  import { resolveTz } from "./tz-utils.js";
54
- import { filesPlayInChat } from "./web-effect.js";
55
52
  import { QUERY_MAX_CHARS, RESULT_MAX, SEARCH_SOURCES } from "./search/search-store.js";
56
53
  import { xmsgShown } from "./search/search-docs.js";
57
54
  import { WebSendDedupe } from "./web-send-dedupe.js";
@@ -233,6 +230,8 @@ function visibleNeeds(ctx, gateway, items) {
233
230
  const { hidden } = boardHiding(ctx, true);
234
231
  return items.filter((i) => {
235
232
  const item = i;
233
+ if (item.type === "config")
234
+ return false; // private operator configuration diagnostics never cross a public link
236
235
  if (item.type !== "task")
237
236
  return true;
238
237
  const code = item.card?.code;
@@ -317,6 +316,8 @@ export const SSE_HEARTBEAT_MS = 10_000;
317
316
  async function parseBody(req) {
318
317
  return JSON.parse((await readBoundedWebBody(req, 512 * 1024)).toString("utf8"));
319
318
  }
319
+ /** #2855: the guest accounts with a command in progress (one at a time each). */
320
+ const guestCommandsRunning = new Set();
320
321
  /** Send JSON response. */
321
322
  function json(res, status, data) {
322
323
  jsonText(res, status, JSON.stringify(data));
@@ -941,159 +942,6 @@ function handleOwnerCss(req, res, url, ctx, path, method) {
941
942
  })().catch(fail);
942
943
  return true;
943
944
  }
944
- /** #1629: static current-placement posters, separate from the local human-only runtime API. */
945
- function handleEffectPosters(res, url, ctx, path, method) {
946
- res.setHeader("Cache-Control", "private, no-store");
947
- res.setHeader("X-Content-Type-Options", "nosniff");
948
- const image = /^\/ui\/effect-posters\/([0-9a-f]{32})\.png$/.exec(path);
949
- const names = [...url.searchParams.keys()], instance = url.searchParams.get("instance");
950
- if (method !== "GET" || path !== "/ui/effect-posters" && !image || names.length !== 1 || names[0] !== "instance" || !instance || instance.length > 128) {
951
- json(res, 404, { error: "not_found" });
952
- return true;
953
- }
954
- const service = ctx.effectService;
955
- if (!service) {
956
- json(res, 404, { error: "effects_unavailable" });
957
- return true;
958
- }
959
- if (image) {
960
- const r = service.placedPoster(image[1], instance);
961
- if (!r.ok)
962
- json(res, 404, { error: "not_found" });
963
- else {
964
- res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
965
- res.end(r.bytes);
966
- }
967
- }
968
- else {
969
- const result = service.posterPlacements(instance);
970
- json(res, result.error === "themes_unreadable" ? 503 : result.error ? 404 : 200, result);
971
- }
972
- return true;
973
- }
974
- /** #1629 Try: separate local-admin routes. These never borrow ordinary read-tier/placement authority. */
975
- function handleEffectTrials(req, res, url, ctx, path, method) {
976
- res.setHeader("Cache-Control", "private, no-store");
977
- res.setHeader("X-Content-Type-Options", "nosniff");
978
- const reading = method === "GET";
979
- if (gatewayRequestContext(req)) {
980
- json(res, reading ? 404 : 403, { error: "trials_not_over_public_link" });
981
- return true;
982
- }
983
- const who = () => {
984
- const sessions = ctx.webSessions;
985
- const v = evaluateWebRequest(req, url, ctx.webToken ?? null, sessions, { touch: false });
986
- if (v.kind !== "allow")
987
- return { error: WEB_TOKEN_INVALID_MESSAGE, status: 401 };
988
- if (v.via !== "session" || v.session.tier !== "admin" || v.session.surface !== "local")
989
- return { error: "trials_need_a_local_admin", status: 403 };
990
- const person = webPersonKey(v.session.owner);
991
- let css = "unknown";
992
- try {
993
- const value = ctx.ownerCss?.effectState(person);
994
- if (value === "off" || value === "on")
995
- css = value;
996
- }
997
- catch { /* unavailable is never off */ }
998
- // CSS is diagnostic for Try; its host-owned shadow shell isolates controls and local paint.
999
- const actor = Object.freeze({ via: "session", surface: "local", tier: "admin", sessionHash: v.session.idHash, epoch: v.session.handle, person, ownerCss: css });
1000
- return { actor, record: v.session, sessions };
1001
- };
1002
- const first = who();
1003
- if ("error" in first) {
1004
- json(res, first.status, { error: first.error });
1005
- return true;
1006
- }
1007
- const service = ctx.effectService;
1008
- if (!service) {
1009
- json(res, 404, { error: "trials_unavailable" });
1010
- return true;
1011
- }
1012
- const start = path === "/ui/effect-trials/start";
1013
- const route = /^\/ui\/effect-trials\/([0-9a-f]{32})\/(resource|poster\.png|profile|stop)$/.exec(path);
1014
- const resource = reading && route && ["resource", "poster.png", "profile"].includes(route[2]);
1015
- const stop = method === "POST" && route?.[2] === "stop";
1016
- if (!(resource || stop || method === "POST" && start)) {
1017
- json(res, 404, { error: "not_found" });
1018
- return true;
1019
- }
1020
- const allowed = resource ? ["ownerEpoch", "instance"] : ["ownerEpoch"];
1021
- if ([...url.searchParams.keys()].some(k => !allowed.includes(k)) || allowed.some(k => url.searchParams.getAll(k).length !== 1)
1022
- || url.searchParams.get("ownerEpoch") !== first.actor.epoch) {
1023
- json(res, reading ? 404 : 409, { error: reading ? "not_found" : "trials_person_changed" });
1024
- return true;
1025
- }
1026
- const instance = url.searchParams.get("instance") ?? "";
1027
- const answer = (result) => json(res, result.error ? result.error === "not_found" ? 404
1028
- : ["profiles_unreadable", "profiles_busy", "profiles_write_failed"].includes(String(result.error)) ? 503 : 400 : 200, result);
1029
- if (resource) {
1030
- if (route[2] === "poster.png") {
1031
- const result = service.trialPoster(route[1], instance, first.actor);
1032
- if (!result.ok)
1033
- json(res, 404, { error: "not_found" });
1034
- else {
1035
- res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
1036
- res.end(result.bytes);
1037
- }
1038
- }
1039
- else
1040
- answer(route[2] === "profile" ? service.trialProfile(route[1], instance, first.actor) : service.trialResource(route[1], instance, first.actor));
1041
- return true;
1042
- }
1043
- (async () => {
1044
- let bytes;
1045
- try {
1046
- bytes = await readBoundedWebBody(req, 16 * 1024);
1047
- }
1048
- catch {
1049
- json(res, 413, { error: "trial_body_bound" });
1050
- return;
1051
- }
1052
- if (!permitWebContinuation(req, res, ctx))
1053
- return;
1054
- const now = who();
1055
- if ("error" in now) {
1056
- json(res, now.status, { error: now.error });
1057
- return;
1058
- }
1059
- if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
1060
- json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
1061
- return;
1062
- }
1063
- if (now.record !== first.record || now.sessions !== first.sessions || now.actor.sessionHash !== first.actor.sessionHash
1064
- || now.actor.person !== first.actor.person || now.actor.epoch !== first.actor.epoch) {
1065
- json(res, 409, { error: "trials_person_changed" });
1066
- return;
1067
- }
1068
- let body;
1069
- try {
1070
- body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1071
- }
1072
- catch {
1073
- json(res, 400, { error: "trial_json" });
1074
- return;
1075
- }
1076
- if (start) {
1077
- const parsed = z.strictObject({ offer: z.string().regex(/^[0-9a-f]{32}$/), instance: z.string().min(1).max(128) }).safeParse(body);
1078
- if (!parsed.success) {
1079
- json(res, 400, { error: "trial_args" });
1080
- return;
1081
- }
1082
- answer(service.trialStart(parsed.data.offer, parsed.data.instance, now.actor));
1083
- }
1084
- else {
1085
- if (!z.strictObject({}).safeParse(body).success) {
1086
- json(res, 400, { error: "trial_args" });
1087
- return;
1088
- }
1089
- answer(service.trialStop(route[1], now.actor));
1090
- }
1091
- })().catch(() => { try {
1092
- json(res, 500, { error: "trial_request_failed" });
1093
- }
1094
- catch { /* response ended */ } });
1095
- return true;
1096
- }
1097
945
  /** #2599: the same closed browser controls as setup, for a current local human admin. */
1098
946
  function handleBrowserBackendSignIns(req, res, url, ctx) {
1099
947
  res.setHeader("Cache-Control", "private, no-store");
@@ -1289,148 +1137,6 @@ function handleBackendLogin(req, res, url, ctx, path, method) {
1289
1137
  catch { /* response ended */ } });
1290
1138
  return true;
1291
1139
  }
1292
- /** #1629 B2: effect routes require a human session; route bodies never supply an actor or reference holder. */
1293
- function handleEffects(req, res, url, ctx, path, method) {
1294
- res.setHeader("Cache-Control", "private, no-store");
1295
- res.setHeader("X-Content-Type-Options", "nosniff");
1296
- const reading = method === "GET";
1297
- // Reports are bounded untrusted diagnostics, not an appearance or admission write (r2 §6).
1298
- const profilePost = method === "POST" && /^\/ui\/effects\/profile\/[0-9a-f]{32}$/.test(path);
1299
- const gateway = gatewayRequestContext(req), guest = guestRequestOf(req);
1300
- const instanceQuery = url.searchParams.get("instance"), keys = [...url.searchParams.keys()];
1301
- const chatRead = reading && (path === "/ui/effects/placements" || /^\/ui\/effects\/resource\/[0-9a-f]{32}$/.test(path))
1302
- && keys.length === 2 && keys.includes("instance") && keys.includes("ownerEpoch") && !!instanceQuery && instanceQuery.length <= 128;
1303
- // Public viewing grants current Chat reads, never ordinary library/owner/Trial access or writes.
1304
- if (gateway && (!chatRead || !gateway.isCurrent())) {
1305
- json(res, reading ? 404 : 403, { error: "effects_not_over_public_link" });
1306
- return true;
1307
- }
1308
- const who = () => {
1309
- const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1310
- if (verdict.kind !== "allow")
1311
- return { status: 401, error: WEB_TOKEN_INVALID_MESSAGE };
1312
- if (verdict.via !== "session" || !verdict.session)
1313
- return { status: 403, error: "effects_need_a_signed_in_person" };
1314
- if (!reading && !profilePost && verdict.session.tier !== "admin")
1315
- return { status: 403, error: "effects_read_only" };
1316
- if (verdict.session.tier === "guest" && (!chatRead || !guest || !guest.current() || !guest.scope().has(instanceQuery)))
1317
- return { status: 404, error: "not_found" };
1318
- return { person: webPersonKey(verdict.session.owner), canWrite: verdict.session.tier === "admin", epoch: verdict.session.handle, guest: verdict.session.tier === "guest" };
1319
- };
1320
- const first = who();
1321
- if ("status" in first) {
1322
- json(res, first.status, { error: first.error });
1323
- return true;
1324
- }
1325
- const service = ctx.effectService;
1326
- if (!service) {
1327
- json(res, 404, { error: "effects_unavailable" });
1328
- return true;
1329
- }
1330
- const unavailable = new Set(["themes_unreadable", "profiles_unreadable", "library_unreadable", "references_unreadable", "library_busy", "profiles_busy", "profiles_write_failed", "library_write_failed", "prefs_unreadable"]);
1331
- const answer = (result) => json(res, typeof result.error === "string" ? result.error === "not_found" ? 404 : result.error === "profile_rate_limited" ? 429 : unavailable.has(result.error) ? 503 : 400 : 200, result);
1332
- const idPath = /^\/ui\/effects\/(resource|profile)\/([0-9a-f]{32})$/.exec(path);
1333
- const instance = url.searchParams.get("instance") ?? undefined, slot = url.searchParams.get("slot");
1334
- if (slot !== null && instance !== undefined) {
1335
- json(res, 404, { error: "not_found" });
1336
- return true;
1337
- }
1338
- const runtimeRead = reading && (slot !== null || path === "/ui/effects/owner" || path === "/ui/effects/placements" || idPath?.[1] === "resource" || instance !== undefined && /^\/ui\/effects\/image\//.test(path));
1339
- if ((runtimeRead || profilePost) && (url.searchParams.getAll("ownerEpoch").length !== 1 || url.searchParams.get("ownerEpoch") !== first.epoch)) {
1340
- const privateResource = reading && (slot !== null || idPath?.[1] === "resource" || /^\/ui\/effects\/image\//.test(path));
1341
- json(res, privateResource ? 404 : 409, { error: privateResource ? "not_found" : "effects_person_changed" });
1342
- return true;
1343
- }
1344
- if (reading) {
1345
- const image = /^\/ui\/effects\/image\/([0-9a-f]{32})\/(poster|cursor)\.png$/.exec(path);
1346
- if (image) {
1347
- const r = slot === null ? service.image(first.person, image[1], image[2], instance) : service.ownerImage(first.person, image[1], image[2], slot);
1348
- if (!r.ok)
1349
- json(res, 404, { error: "not_found" });
1350
- else {
1351
- res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
1352
- res.end(r.bytes);
1353
- }
1354
- return true;
1355
- }
1356
- if (path === "/ui/effects")
1357
- answer({ ...service.list(first.person), canWrite: first.canWrite });
1358
- else if (path === "/ui/effects/owner" && instance === undefined && slot === null)
1359
- answer(service.ownerSelections(first.person));
1360
- else if (path === "/ui/effects/placements" && instance !== undefined)
1361
- answer(service.placements(first.person, instance, !!gateway || first.guest));
1362
- else if (idPath?.[1] === "resource")
1363
- answer(slot === null ? service.resource(first.person, idPath[2], instance, !!gateway || first.guest) : service.ownerResource(first.person, idPath[2], slot));
1364
- else if (idPath?.[1] === "profile" && instance === undefined)
1365
- answer(service.profile(first.person, idPath[2]));
1366
- else
1367
- json(res, 404, { error: "not_found" });
1368
- return true;
1369
- }
1370
- const deletion = /^\/ui\/effects\/([0-9a-f]{32})$/.exec(path);
1371
- if (method === "DELETE" && deletion) {
1372
- answer(service.delete(first.person, deletion[1]));
1373
- return true;
1374
- }
1375
- if (method !== "POST" || !["/ui/effects/upload", "/ui/effects/apply", "/ui/effects/remove", "/ui/effects/share"].includes(path) && idPath?.[1] !== "profile") {
1376
- json(res, 404, { error: "not_found" });
1377
- return true;
1378
- }
1379
- (async () => {
1380
- let bytes;
1381
- try {
1382
- bytes = await readBoundedWebBody(req, path === "/ui/effects/upload" ? EFFECT_WIRE_LIMIT : 16 * 1024);
1383
- }
1384
- catch {
1385
- json(res, 413, { error: "effect_body_bound" });
1386
- return;
1387
- }
1388
- if (!permitWebContinuation(req, res, ctx))
1389
- return;
1390
- const now = who();
1391
- if ("status" in now) {
1392
- json(res, now.status, { error: now.error });
1393
- return;
1394
- }
1395
- if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
1396
- json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
1397
- return;
1398
- }
1399
- if (now.person !== first.person || now.epoch !== first.epoch) {
1400
- json(res, 409, { error: "effects_person_changed" });
1401
- return;
1402
- }
1403
- if (path === "/ui/effects/upload") {
1404
- answer(service.upload(now.person, bytes));
1405
- return;
1406
- }
1407
- let body;
1408
- try {
1409
- body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1410
- }
1411
- catch {
1412
- json(res, 400, { error: "effect_json" });
1413
- return;
1414
- }
1415
- if (path === "/ui/effects/apply")
1416
- answer(service.apply(now.person, body));
1417
- else if (path === "/ui/effects/remove")
1418
- answer(service.remove(now.person, body));
1419
- else if (path === "/ui/effects/share")
1420
- answer(service.share(now.person, body));
1421
- else {
1422
- if (body && typeof body === "object" && body.kind === "playback" && (url.searchParams.getAll("instance").length !== 1 || slot !== null)) {
1423
- json(res, 400, { error: "playback_report" });
1424
- return;
1425
- }
1426
- answer(service.report(now.person, idPath[2], body, instance, now.epoch));
1427
- }
1428
- })().catch(() => { try {
1429
- json(res, 500, { error: "effect_request_failed" });
1430
- }
1431
- catch { /* response ended */ } });
1432
- return true;
1433
- }
1434
1140
  /**
1435
1141
  * #1629 r4 §4 (r4-5): mods — a person's import (stage, then commit), uninstall and export. Every route is a signed-in
1436
1142
  * session's, writes an admin's: never the CLI's header token (the way an agent could reach it) and never the public link
@@ -1513,18 +1219,13 @@ function handleMods(req, res, url, ctx, path, method) {
1513
1219
  json(res, 404, { error: "no_such_mod" });
1514
1220
  return true;
1515
1221
  }
1516
- // The file is the installed bundle, effects and all — also effects its import left out, which nobody installed.
1517
- // So the export ACL judges what the file carries, not what was installed: it downloads only when this person may
1518
- // read every effect in it (their own claim on that content, or a shared one). A file without effects is any admin's.
1222
+ // The file is the installed bundle, effects and all. #2799: a file that carries Canvas Worker effects (or can't be
1223
+ // read to tell) is not handed out; one without is any admin's.
1519
1224
  const fx = mods.bundleEffects(fileMatch[1]);
1520
- if (!canvasWorkerEnabled(ctx.fleetConfig?.web) && (fx === "unknown" || (fx?.length ?? 0) > 0)) {
1225
+ if (fx === "unknown" || (fx?.length ?? 0) > 0) {
1521
1226
  json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
1522
1227
  return true;
1523
1228
  }
1524
- if (fx === "unknown" || (fx ?? []).some(id => !ctx.modPlacedEffect?.(first.person, id))) {
1525
- json(res, 403, { error: "mod_effects_not_yours", detail: "this mod's file carries effects that are not yours to share: export your own look instead" });
1526
- return true;
1527
- }
1528
1229
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "X-Content-Type-Options": "nosniff", "Cache-Control": "private, no-store",
1529
1230
  "Content-Disposition": `attachment; filename="${rec.name}-${rec.version}.agendmod.json"` });
1530
1231
  res.end(text);
@@ -1648,14 +1349,9 @@ export function exportMod(ctx, person, body) {
1648
1349
  const store = ctx.themeStore;
1649
1350
  const rec = store.get(name);
1650
1351
  if (inc.theme === true) {
1651
- // The effects the theme places travel with it, as the library keeps them (a person may export what is placed).
1652
- // Only effects this person may carry away travel (Sol's ACL); the rest are counted, never packaged.
1653
- // #2017: an older placement of an effect that doesn't play in chat stays out (a mod can't place it): counted.
1654
- const effects = (canvasWorkerEnabled(ctx.fleetConfig?.web) ? rec.placements.effects : []).flatMap(p => { const e = ctx.modPlacedEffect?.(person, p.id); if (!e || !filesPlayInChat(e.files)) {
1655
- omitted++;
1656
- return [];
1657
- } return [{ name: e.name, files: e.files, params: p.params ?? {} }]; });
1658
- parts.theme = { theme: rec.theme, picture: id => { const a = rec.assets.find(x => x.id === id); const b = store.asset(name, id); return a && b ? { bytes: b.bytes, kind: a.kind } : null; }, effects };
1352
+ // #2799: Canvas Worker effects were removed; an older placement travels nowhere (it is counted as left out).
1353
+ omitted += rec.placements.effects.length;
1354
+ parts.theme = { theme: rec.theme, picture: id => { const a = rec.assets.find(x => x.id === id); const b = store.asset(name, id); return a && b ? { bytes: b.bytes, kind: a.kind } : null; }, effects: [] };
1659
1355
  }
1660
1356
  if (inc.identity === true)
1661
1357
  parts.identity = ctx.modCardText?.(name) ?? {};
@@ -1728,6 +1424,109 @@ export function webPeerOf(req) {
1728
1424
  const gateway = gatewayRequestContext(req);
1729
1425
  return gateway ? (gateway.named === true ? "named_link" : "quick_link") : classifyWebPeer(req);
1730
1426
  }
1427
+ /** #2855: the guest accounts with a search running, and how many guest searches run in all (GUEST_SEARCHES). */
1428
+ const guestSearchesRunning = new Set();
1429
+ let guestSearchesInFlight = 0;
1430
+ /**
1431
+ * #2855: a trusted guest's search — chat messages of its in-scope agents that it can see now (guestSeesMessage, on the
1432
+ * history's own row: one no longer kept is not shown, nothing else is), at most GUEST_SEARCHES.maxResults. Per account
1433
+ * GUEST_SEARCHES a minute and one at a time; at most GUEST_SEARCHES.global guest searches running in all, so the
1434
+ * index's queue always has room for the admins'. Its context: the same rows only.
1435
+ */
1436
+ function guestSearch(req, res, url, ctx, guest, path) {
1437
+ const refusal = () => {
1438
+ if (!guest.current())
1439
+ return [401, WEB_TOKEN_INVALID_MESSAGE];
1440
+ if (!guest.trusted?.())
1441
+ return [403, "guest_not_trusted"];
1442
+ if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
1443
+ return [404, "search_not_over_public_link"];
1444
+ if (ctx.fleetConfig?.web?.search?.enabled === false || !ctx.searchIndex || ctx.searchIndex.status() === "unavailable")
1445
+ return [404, "search_unavailable"];
1446
+ return null;
1447
+ };
1448
+ const first = refusal();
1449
+ if (first) {
1450
+ json(res, first[0], { error: first[1] });
1451
+ return;
1452
+ }
1453
+ const account = `${guest.owner.adapterId}:${guest.owner.userId}`;
1454
+ const limit = ctx.guestSearches?.check(account);
1455
+ if (limit && !limit.ok) {
1456
+ res.setHeader("Retry-After", String(limit.retry_after_s));
1457
+ json(res, 429, { error: "guest_search_rate_limited", retry_after_s: limit.retry_after_s });
1458
+ return;
1459
+ }
1460
+ if (guestSearchesRunning.has(account)) {
1461
+ json(res, 429, { error: "guest_search_running" });
1462
+ return;
1463
+ }
1464
+ if (guestSearchesInFlight >= GUEST_SEARCHES.global) {
1465
+ res.setHeader("Retry-After", "2");
1466
+ json(res, 503, { error: "search_busy" });
1467
+ return;
1468
+ }
1469
+ const index = ctx.searchIndex, history = ctx.webChatHistory;
1470
+ // What this guest may see of one indexed chat row: its history row, now, by the guest rule; else nothing.
1471
+ const visible = (hit) => {
1472
+ if (hit.source !== "chat" || !guest.scope().has(hit.instance) || !history)
1473
+ return false;
1474
+ const row = history.find(hit.instance, String(hit.ref.boot ?? ""), Number(hit.ref.id));
1475
+ return !!row && guestSeesMessage(row, guest, guest.scope());
1476
+ };
1477
+ const finish = (status, body) => { const now = refusal(); if (now)
1478
+ json(res, now[0], { error: now[1] });
1479
+ else
1480
+ json(res, status, body); };
1481
+ const failed = (reason) => finish(reason === "timeout" || reason === "busy" ? 503 : 404, { error: `search_${reason}` });
1482
+ let run;
1483
+ if (path === "/ui/search/context") {
1484
+ const key = url.searchParams.get("key") ?? "";
1485
+ if (!key || key.length > 300) {
1486
+ json(res, 400, { error: "key required" });
1487
+ return;
1488
+ }
1489
+ run = index.context(key).then(r => {
1490
+ if (!r.ok)
1491
+ return failed(r.reason);
1492
+ const value = r.value;
1493
+ if (!value?.doc || !visible(value.doc))
1494
+ return finish(404, { error: "not_found" });
1495
+ finish(200, { doc: value.doc, before: value.before.filter(visible), after: value.after.filter(visible), building: r.building });
1496
+ }, () => failed("unavailable"));
1497
+ }
1498
+ else {
1499
+ // Never a team (whose members, and whether it exists, are the admin's) or "only mine" (an admin's own messages).
1500
+ const own = new URL(url.href);
1501
+ own.searchParams.delete("team");
1502
+ own.searchParams.delete("mine");
1503
+ const parsed = parseSearchQuery(own, ctx);
1504
+ if ("error" in parsed) {
1505
+ json(res, 400, { error: parsed.error });
1506
+ return;
1507
+ }
1508
+ const scope = guest.scope();
1509
+ const asked = parsed.filters.instances?.filter(n => scope.has(n)) ?? [...scope];
1510
+ if (!asked.length) {
1511
+ json(res, 200, { hits: [], limited: false });
1512
+ return;
1513
+ }
1514
+ // Chat only, its scope only; more asked of the index than shown, since rows it can't see are left out after.
1515
+ run = index.query({ q: parsed.filters.q, instances: asked, sources: ["chat"], limit: SEARCH_LIMIT_DEFAULT,
1516
+ ...(parsed.filters.since !== undefined ? { since: parsed.filters.since } : {}), ...(parsed.filters.until !== undefined ? { until: parsed.filters.until } : {}) }).then(r => {
1517
+ if (!r.ok)
1518
+ return failed(r.reason);
1519
+ const hits = r.value.hits.filter(visible);
1520
+ finish(200, { hits: hits.slice(0, GUEST_SEARCHES.maxResults).map(h => ({ ...h, live: true })), limited: r.value.limited || hits.length > GUEST_SEARCHES.maxResults, building: r.building });
1521
+ }, () => failed("unavailable"));
1522
+ }
1523
+ ctx.guestSearches?.record(account);
1524
+ guestSearchesRunning.add(account);
1525
+ guestSearchesInFlight++;
1526
+ void run.catch(() => { if (!res.headersSent)
1527
+ json(res, 500, { error: "search_failed" }); })
1528
+ .finally(() => { guestSearchesRunning.delete(account); guestSearchesInFlight--; });
1529
+ }
1731
1530
  /**
1732
1531
  * #2122: what let this request in, asked now — its session (by handle, under the token it was signed in with) or the
1733
1532
  * header token; null when nothing does any more (the gate also refuses a public link that is no longer the current
@@ -1911,10 +1710,6 @@ function statusTick(ctx, t) {
1911
1710
  export function handleWebRequest(req, res, url, ctx) {
1912
1711
  const path = url.pathname;
1913
1712
  const method = req.method ?? "GET";
1914
- if (!canvasWorkerEnabled(ctx.fleetConfig?.web) && isCanvasWorkerHttpPath(path)) {
1915
- json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
1916
- return true;
1917
- }
1918
1713
  // Auth check for all /ui routes. Defence in depth behind the fleet-manager
1919
1714
  // gate: the same session cookie or header token, and an unset token closes
1920
1715
  // the panel instead of comparing null against a missing credential.
@@ -2190,7 +1985,10 @@ export function handleWebRequest(req, res, url, ctx) {
2190
1985
  const guest = guestRequestOf(req);
2191
1986
  const messages = !ctx.webChatHistory ? [] : guest ? guestHistory(ctx.webChatHistory, instance, limit, guest, guest.scope())
2192
1987
  : visibleMessages(ctx, !!gatewayRequestContext(req), ctx.webChatHistory.list(instance, limit));
2193
- json(res, 200, { messages, boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
1988
+ // #2848: the chat has messages, none of them a guest's to see (all between agents, or kept before they were marked):
1989
+ // said, so its empty chat is explained rather than blank. Only that there are some — never how many, or what.
1990
+ const allHidden = !!guest && messages.length === 0 && (ctx.webChatHistory?.list(instance, 1).length ?? 0) > 0;
1991
+ json(res, 200, { messages, boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0, ...(allHidden ? { allHidden: true } : {}) });
2194
1992
  return true;
2195
1993
  }
2196
1994
  // ── #1218: the Log tab ─────────────────────────────────
@@ -2854,6 +2652,13 @@ export function handleWebRequest(req, res, url, ctx) {
2854
2652
  json(res, 404, { error: "Preferences are not kept on this fleet" });
2855
2653
  return true;
2856
2654
  }
2655
+ // #2855: a guest saves its own preferences (GUEST_OWN_PREF_KEYS) into its own account's bucket — on any listener,
2656
+ // the public link too, since nothing of the host's is written. At most GUEST_PREF_WRITES a minute per account.
2657
+ const guest = guestRequestOf(req);
2658
+ if (guest) {
2659
+ saveGuestPrefs(req, res, ctx, prefs, guest);
2660
+ return true;
2661
+ }
2857
2662
  if (gatewayRequestContext(req)) {
2858
2663
  json(res, 403, { error: "prefs_not_over_public_link" });
2859
2664
  return true;
@@ -2894,30 +2699,16 @@ export function handleWebRequest(req, res, url, ctx) {
2894
2699
  return;
2895
2700
  }
2896
2701
  const personal = [...parsed.patch.keys()].some(isOwnerEffectKey);
2897
- if (personal && !canvasWorkerEnabled(ctx.fleetConfig?.web)) {
2702
+ // #2799: a person's own effect selections were Canvas Worker effects' (removed): refused, as before.
2703
+ if (personal) {
2898
2704
  json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
2899
2705
  return;
2900
2706
  }
2901
- if (personal && now.person === null) {
2902
- json(res, 403, { error: "prefs_need_a_signed_in_person" });
2903
- return;
2904
- }
2905
- // A scope marker rejects a queued old page after a different login, including the same person's new session.
2906
- // It supplies neither identity nor authority; the current verified session still chooses the bucket.
2907
- if (personal && body.ownerEpoch !== now.epoch) {
2908
- json(res, 409, { error: "prefs_person_changed" });
2909
- return;
2910
- }
2911
- const before = prefs.readForViewer(now.person);
2912
- const written = prefs.writeForViewer(now.person, parsed.patch, { onlyIfUnset: body.onlyIfUnset === true,
2913
- validateEffect: (person, key, id) => ctx.effectService ? ctx.effectService.validateOwnerSelection(person, key, id) : "effects_unavailable" });
2707
+ const written = prefs.writeForViewer(now.person, parsed.patch, { onlyIfUnset: body.onlyIfUnset === true, validateEffect: () => "effects_unavailable" });
2914
2708
  if (!written.ok) {
2915
2709
  json(res, written.status, { error: written.error });
2916
2710
  return;
2917
2711
  }
2918
- const dropped = written.changed.filter(isOwnerEffectKey).flatMap(key => typeof before.values[key] === "string" && before.values[key] !== written.view.values[key] ? [before.values[key]] : []);
2919
- if (dropped.length)
2920
- ctx.effectService?.collectOwnerReplaced(dropped);
2921
2712
  if (written.changed.length)
2922
2713
  ctx.emitSseEvent("status", ctx.getUiStatus());
2923
2714
  json(res, 200, { ...written.view, ...prefsScope(now), changed: written.changed });
@@ -2943,19 +2734,11 @@ export function handleWebRequest(req, res, url, ctx) {
2943
2734
  // #1629 r4-5: mods — a person's import, uninstall and export (handleMods: sessions only, never the public link).
2944
2735
  if (path === "/ui/mods" || path.startsWith("/ui/mods/"))
2945
2736
  return handleMods(req, res, url, ctx, path, method);
2946
- // Posters remain behind the common session gate, with a closed GET-only public manifest.
2947
- if (path === "/ui/effect-posters" || path.startsWith("/ui/effect-posters/"))
2948
- return handleEffectPosters(res, url, ctx, path, method);
2949
- // Canvas effects are absent from the public gateway manifest and independently refused above.
2950
- if (path === "/ui/effect-trials" || path.startsWith("/ui/effect-trials/"))
2951
- return handleEffectTrials(req, res, url, ctx, path, method);
2952
2737
  // #2233: backend sign-in — absent from the public manifest and the guest table, and refused here too.
2953
2738
  if (path === "/ui/browser-backend-signins")
2954
2739
  return handleBrowserBackendSignIns(req, res, url, ctx);
2955
2740
  if (path === "/ui/backend-login" || path.startsWith("/ui/backend-login/"))
2956
2741
  return handleBackendLogin(req, res, url, ctx, path, method);
2957
- if (path === "/ui/effects" || path.startsWith("/ui/effects/"))
2958
- return handleEffects(req, res, url, ctx, path, method);
2959
2742
  // #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
2960
2743
  // token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
2961
2744
  if (method === "POST" && path === "/ui/needs/ack") {
@@ -3050,6 +2833,21 @@ export function handleWebRequest(req, res, url, ctx) {
3050
2833
  return true;
3051
2834
  }
3052
2835
  const run = ctx.webCommand.bind(ctx);
2836
+ // #2855: a guest — an in-scope agent, while guests may chat, the table's "anyone" commands only (runWebCommand), at
2837
+ // most GUEST_COMMAND_RUNS a minute per account and one at a time.
2838
+ const guest = guestRequestOf(req), account = guest ? `${guest.owner.adapterId}:${guest.owner.userId}` : "";
2839
+ if (guest) {
2840
+ const limit = ctx.guestCommandRuns?.check(account);
2841
+ if (limit && !limit.ok) {
2842
+ res.setHeader("Retry-After", String(limit.retry_after_s));
2843
+ json(res, 429, { error: "guest_commands_rate_limited", retry_after_s: limit.retry_after_s });
2844
+ return true;
2845
+ }
2846
+ if (guestCommandsRunning.has(account)) {
2847
+ json(res, 429, { error: "guest_command_running" });
2848
+ return true;
2849
+ }
2850
+ }
3053
2851
  const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
3054
2852
  (async () => {
3055
2853
  let body;
@@ -3079,9 +2877,39 @@ export function handleWebRequest(req, res, url, ctx) {
3079
2877
  const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
3080
2878
  if (!person)
3081
2879
  return;
3082
- const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
3083
- const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person, authority });
3084
- json(res, r.status, r.body);
2880
+ if (guest) {
2881
+ if (!guest.current()) {
2882
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2883
+ return;
2884
+ }
2885
+ const refused = guestChatRefusal(guest, instance);
2886
+ if (refused) {
2887
+ json(res, refused.status, { error: refused.error });
2888
+ return;
2889
+ }
2890
+ if (guestCommandsRunning.has(account)) {
2891
+ json(res, 429, { error: "guest_command_running" });
2892
+ return;
2893
+ }
2894
+ // The budget again, now (#2857 review): commands that ran while this body was on its way have counted.
2895
+ const budget = ctx.guestCommandRuns?.check(account);
2896
+ if (budget && !budget.ok) {
2897
+ res.setHeader("Retry-After", String(budget.retry_after_s));
2898
+ json(res, 429, { error: "guest_commands_rate_limited", retry_after_s: budget.retry_after_s });
2899
+ return;
2900
+ }
2901
+ guestCommandsRunning.add(account);
2902
+ ctx.guestCommandRuns?.record(account);
2903
+ }
2904
+ try {
2905
+ const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
2906
+ const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person, authority, ...(guest ? { guest: true } : {}) });
2907
+ json(res, r.status, r.body);
2908
+ }
2909
+ finally {
2910
+ if (guest)
2911
+ guestCommandsRunning.delete(account);
2912
+ }
3085
2913
  })().catch(err => {
3086
2914
  ctx.logger.error({ err: err.message }, "Web command failed");
3087
2915
  try {
@@ -3706,11 +3534,15 @@ export function handleWebRequest(req, res, url, ctx) {
3706
3534
  // #2478 (the user's request): over the public link too, unless web.search.public_link is false. Asked on arrival and
3707
3535
  // again before the answer is sent.
3708
3536
  if (method === "GET" && (path === "/ui/search" || path === "/ui/search/context")) {
3537
+ // #2855: a trusted guest searches only what it can see (guestSearch).
3538
+ const searchGuest = guestRequestOf(req);
3539
+ if (searchGuest) {
3540
+ guestSearch(req, res, url, ctx, searchGuest, path);
3541
+ return true;
3542
+ }
3709
3543
  const refusal = () => {
3710
3544
  if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
3711
3545
  return [404, "search_not_over_public_link"];
3712
- if (guestRequestOf(req))
3713
- return [403, "guest_forbidden"];
3714
3546
  if (ctx.fleetConfig?.web?.search?.enabled === false || !ctx.searchIndex || ctx.searchIndex.status() === "unavailable")
3715
3547
  return [404, "search_unavailable"];
3716
3548
  return null;
@@ -4121,7 +3953,7 @@ function handleSendMessage(req, res, ctx) {
4121
3953
  let quoted;
4122
3954
  if (replyTo) {
4123
3955
  quoted = ctx.webChatHistory?.find(instance, replyTo.boot, replyTo.id);
4124
- if (!quoted || quoted.effectTrial || (guest && !guestSeesMessage(quoted, guest, guest.scope()))) {
3956
+ if (!quoted || (guest && !guestSeesMessage(quoted, guest, guest.scope()))) {
4125
3957
  json(res, 409, { error: t("web.reply_gone"), reason: "reply_gone" });
4126
3958
  return;
4127
3959
  }
@@ -4533,6 +4365,64 @@ function serveMediaRange(req, res, files, id, file) {
4533
4365
  res.writeHead(ranged ? 206 : 200);
4534
4366
  res.end(got.bytes);
4535
4367
  }
4368
+ /** #2855: a guest's own preferences — its own keys, its own bucket, favourites only in its scope. */
4369
+ function saveGuestPrefs(req, res, ctx, prefs, guest) {
4370
+ const person = webPersonKey(guest.owner), account = `${guest.owner.adapterId}:${guest.owner.userId}`;
4371
+ const limit = ctx.guestPrefWrites?.check(account);
4372
+ if (limit && !limit.ok) {
4373
+ res.setHeader("Retry-After", String(limit.retry_after_s));
4374
+ json(res, 429, { error: "guest_prefs_rate_limited", retry_after_s: limit.retry_after_s });
4375
+ return;
4376
+ }
4377
+ // Too large by its own declared length: answered before a byte is read (the body is drained, not cut off).
4378
+ if (Number(req.headers["content-length"]) > GUEST_PREF_MAX_BYTES) {
4379
+ req.resume();
4380
+ json(res, 413, { error: "guest_prefs_too_large", detail: "at most 16 KB" });
4381
+ return;
4382
+ }
4383
+ (async () => {
4384
+ let body;
4385
+ try {
4386
+ body = JSON.parse((await readBoundedWebBody(req, GUEST_PREF_MAX_BYTES)).toString("utf8"));
4387
+ }
4388
+ catch {
4389
+ json(res, 400, { error: "Invalid JSON, or over 16 KB" });
4390
+ return;
4391
+ }
4392
+ if (!permitWebContinuation(req, res, ctx))
4393
+ return;
4394
+ if (!guest.current()) {
4395
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
4396
+ return;
4397
+ }
4398
+ if (!body || typeof body !== "object" || Array.isArray(body) || !body.set || typeof body.set !== "object" || Array.isArray(body.set)) {
4399
+ json(res, 400, { error: "set required" });
4400
+ return;
4401
+ }
4402
+ const set = body.set, scope = guest.scope();
4403
+ const favourites = set["nav.favorites"];
4404
+ if (Array.isArray(favourites)) {
4405
+ const outside = favourites.find((n) => typeof n !== "string" || !scope.has(n));
4406
+ if (outside !== undefined) {
4407
+ json(res, 400, { error: `not_in_scope: ${String(outside).slice(0, 64)}` });
4408
+ return;
4409
+ }
4410
+ }
4411
+ const written = prefs.writeForGuest(person, new Map(Object.entries(set)), { onlyIfUnset: body.onlyIfUnset === true });
4412
+ if (!written.ok) {
4413
+ json(res, written.status, { error: written.error });
4414
+ return;
4415
+ }
4416
+ ctx.guestPrefWrites?.record(account);
4417
+ json(res, 200, { ...guestOwnPrefs(prefs.readForViewer(person), prefs.readGuestOwn(person), scope), ownerScope: "person", ownerEpoch: null, changed: written.changed });
4418
+ })().catch(err => {
4419
+ ctx.logger.error({ err: err.message }, "Saving a guest's web preferences failed");
4420
+ try {
4421
+ json(res, 500, { error: "Saving preferences failed" });
4422
+ }
4423
+ catch { /* already answered */ }
4424
+ });
4425
+ }
4536
4426
  function prefsViewer(req, url, ctx) {
4537
4427
  const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
4538
4428
  if (v.kind !== "allow")
@@ -4541,6 +4431,12 @@ function prefsViewer(req, url, ctx) {
4541
4431
  // it (#2094) read the host's preferences unfiltered: the sidebar's order and favourites, naming instances out of its
4542
4432
  // scope. Its account (phase 3) names it on its page.
4543
4433
  const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true, ...(v.session.owner ? { owner: v.session.owner } : {}) } : {};
4434
+ // #2855: a guest reads its own preferences (its account's bucket) on every listener, the public link too.
4435
+ if (v.via === "session" && v.session?.tier === "guest" && v.session.owner) {
4436
+ const g = guestRequestOf(req);
4437
+ return { person: webPersonKey(v.session.owner), epoch: v.session.handle, scope: "person", canWrite: false, via: "session", ...guest,
4438
+ guestScope: () => g?.scope() ?? new Set() };
4439
+ }
4544
4440
  if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
4545
4441
  const person = webPersonKey(v.session.owner);
4546
4442
  return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
@@ -4551,7 +4447,10 @@ const prefsScope = (v) => ({ ownerScope: v?.scope ?? "none", ownerEpoch: v?.epoc
4551
4447
  // #1920: a guest reads the host's look and its own, never a preference that names an instance (guestPrefs).
4552
4448
  const prefsView = (prefs, v) => {
4553
4449
  const view = { ...prefs.readForViewer(v?.person ?? null), ...prefsScope(v) };
4554
- return v?.guest ? guestPrefs(view) : view;
4450
+ if (!v?.guest)
4451
+ return view;
4452
+ // #2855: the host's look, then the guest's own preferences (only on a guest with an account: its bucket).
4453
+ return v.person ? guestOwnPrefs(view, prefs.readGuestOwn(v.person), v.guestScope?.() ?? new Set()) : guestPrefs(view);
4555
4454
  };
4556
4455
  function shellHtmlTag(ctx, mode, viewer) {
4557
4456
  if (mode !== "full" || !ctx.webPrefs)
@@ -4570,20 +4469,12 @@ function shellOwnerCss(ctx, mode, viewer) {
4570
4469
  }
4571
4470
  function guestBodyAttrs(ctx, owner, attr) {
4572
4471
  const badge = ctx.guestBadge?.(owner);
4573
- return ` data-tier="guest"${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
4574
- }
4575
- /** A separate session fence for Chat playback: public/guest viewers acquire no owner-pref identity. */
4576
- function shellEffect(req, ctx, mode) {
4577
- if (mode !== "full" || typeof ctx.effectBoot !== "string" || !/^[0-9a-f]{32}$/.test(ctx.effectBoot))
4578
- return null;
4579
- const v = evaluateWebRequest(req, new URL(req.url ?? "/", "http://localhost"), ctx.webToken ?? null, ctx.webSessions, { touch: false });
4580
- if (v.kind !== "allow" || v.via !== "session" || !v.session || !/^[a-f0-9]{16}$/.test(v.session.handle))
4581
- return null;
4582
- return { boot: ctx.effectBoot, epoch: v.session.handle, scope: gatewayRequestContext(req) || v.session.tier === "guest" ? "chat" : "owner" };
4472
+ const trusted = ctx.guestTrusted?.(owner) ? ' data-guest-level="trusted"' : "";
4473
+ return ` data-tier="guest"${trusted}${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
4583
4474
  }
4584
4475
  function shellBodyTag(req, ctx, mode, viewer) {
4585
4476
  const p = shellPreview(req, ctx, mode);
4586
- const ownerCss = shellOwnerCss(ctx, mode, viewer), effect = shellEffect(req, ctx, mode);
4477
+ const ownerCss = shellOwnerCss(ctx, mode, viewer);
4587
4478
  let ownerCssState = mode === "full" && (gatewayRequestContext(req) || viewer?.guest) ? "off" : "unknown";
4588
4479
  if (mode === "full" && !viewer?.guest && viewer?.person != null && ctx.ownerCss) {
4589
4480
  ownerCssState = ctx.ownerCss.effectState(viewer.person);
@@ -4592,17 +4483,15 @@ function shellBodyTag(req, ctx, mode, viewer) {
4592
4483
  }
4593
4484
  const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
4594
4485
  return `<body data-mode="${mode}"${mode === "full" && gatewayRequestContext(req) ? ' data-web-transport="poll"' : ""} data-dashboard-origin="${attr(p?.dashboardOrigin)}" data-preview-origin="${attr(p?.previewOrigin)}"`
4595
- + `${effect ? ` data-effect-boot="${effect.boot}" data-effect-epoch="${effect.epoch}" data-effect-scope="${effect.scope}"` : ""}`
4596
4486
  + ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
4597
4487
  // #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
4598
4488
  + ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
4599
4489
  // #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
4600
4490
  + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
4601
4491
  // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔ — never to a guest (#1920).
4602
- + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false || viewer?.guest ? "0" : "1"}"`
4492
+ + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"` // #2855: guests' too
4603
4493
  // #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
4604
4494
  + ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
4605
- + ` data-canvas-worker="${canvasWorkerEnabled(ctx.fleetConfig?.web) ? "1" : "0"}"`
4606
4495
  // #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
4607
4496
  // checks before sending a file, and the numbers its words say. The server checks again on every upload.
4608
4497
  + ` data-upload-limits="${attr(JSON.stringify(uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined })))}"`
@@ -4632,10 +4521,7 @@ export function serveAppShell(req, res, ctx, mode) {
4632
4521
  // A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
4633
4522
  // the page may load); every other page frames exactly the origin this load chose (#1723).
4634
4523
  const previewFrames = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
4635
- // This fixed trusted document is independent of HTML preview. Its own response confines all Worker execution.
4636
- const effectOrigin = gatewayRequestContext(req)?.expectedOrigin ?? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req));
4637
- const effectFrame = shellEffect(req, ctx, mode) && effectOrigin ? `${effectOrigin}/effect-bootstrap` : null;
4638
- const frameSrc = [previewFrames, effectFrame].filter(Boolean).join(" ");
4524
+ const frameSrc = previewFrames ?? "";
4639
4525
  sendPanelHtml(res, withAppIconLinks(html).replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
4640
4526
  }
4641
4527
  catch {