@songsid/agend 2.1.12 → 2.2.0-alpha.2

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 (436) hide show
  1. package/README.md +4 -2
  2. package/README.zh-TW.md +4 -2
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +68 -12
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-token-header.d.ts +13 -0
  8. package/dist/agent-token-header.js +16 -0
  9. package/dist/agent-token-header.js.map +1 -0
  10. package/dist/auth/one-time-code.d.ts +19 -0
  11. package/dist/auth/one-time-code.js +49 -0
  12. package/dist/auth/one-time-code.js.map +1 -0
  13. package/dist/auth-api.d.ts +52 -0
  14. package/dist/auth-api.js +346 -0
  15. package/dist/auth-api.js.map +1 -0
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/claude-code.d.ts +4 -0
  19. package/dist/backend/claude-code.js +97 -0
  20. package/dist/backend/claude-code.js.map +1 -1
  21. package/dist/backend/codex.js +59 -30
  22. package/dist/backend/codex.js.map +1 -1
  23. package/dist/backend/grok.js +1 -1
  24. package/dist/backend/grok.js.map +1 -1
  25. package/dist/backend/kiro-agent.d.ts +66 -0
  26. package/dist/backend/kiro-agent.js +302 -0
  27. package/dist/backend/kiro-agent.js.map +1 -0
  28. package/dist/backend/kiro-identity.d.ts +80 -0
  29. package/dist/backend/kiro-identity.js +537 -0
  30. package/dist/backend/kiro-identity.js.map +1 -0
  31. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  32. package/dist/backend/kiro-v2-store-worker.js +87 -0
  33. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  34. package/dist/backend/kiro-v2-store.d.ts +54 -0
  35. package/dist/backend/kiro-v2-store.js +131 -0
  36. package/dist/backend/kiro-v2-store.js.map +1 -0
  37. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  38. package/dist/backend/kiro-v3-identity.js +22 -0
  39. package/dist/backend/kiro-v3-identity.js.map +1 -1
  40. package/dist/backend/kiro.d.ts +90 -7
  41. package/dist/backend/kiro.js +500 -124
  42. package/dist/backend/kiro.js.map +1 -1
  43. package/dist/backend/muse.js +1 -1
  44. package/dist/backend/muse.js.map +1 -1
  45. package/dist/backend/types.d.ts +51 -3
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-analysis.d.ts +92 -0
  48. package/dist/cache-analysis.js +147 -0
  49. package/dist/cache-analysis.js.map +1 -0
  50. package/dist/cache-ledger.d.ts +108 -0
  51. package/dist/cache-ledger.js +230 -0
  52. package/dist/cache-ledger.js.map +1 -0
  53. package/dist/cache-prices.d.ts +32 -0
  54. package/dist/cache-prices.js +73 -0
  55. package/dist/cache-prices.js.map +1 -0
  56. package/dist/cache-scan.d.ts +39 -0
  57. package/dist/cache-scan.js +115 -0
  58. package/dist/cache-scan.js.map +1 -0
  59. package/dist/cache-service.d.ts +115 -0
  60. package/dist/cache-service.js +417 -0
  61. package/dist/cache-service.js.map +1 -0
  62. package/dist/channel/adapters/discord.d.ts +1 -0
  63. package/dist/channel/adapters/discord.js +29 -10
  64. package/dist/channel/adapters/discord.js.map +1 -1
  65. package/dist/channel/adapters/telegram.d.ts +1 -0
  66. package/dist/channel/adapters/telegram.js +13 -1
  67. package/dist/channel/adapters/telegram.js.map +1 -1
  68. package/dist/channel/types.d.ts +9 -1
  69. package/dist/classic-channel-manager.d.ts +18 -10
  70. package/dist/classic-channel-manager.js +45 -18
  71. package/dist/classic-channel-manager.js.map +1 -1
  72. package/dist/cli-entry.d.ts +19 -0
  73. package/dist/cli-entry.js +36 -0
  74. package/dist/cli-entry.js.map +1 -0
  75. package/dist/cli.js +471 -146
  76. package/dist/cli.js.map +1 -1
  77. package/dist/command-table.d.ts +5 -3
  78. package/dist/command-table.js +29 -27
  79. package/dist/command-table.js.map +1 -1
  80. package/dist/config-validator.js +46 -0
  81. package/dist/config-validator.js.map +1 -1
  82. package/dist/config.js +12 -0
  83. package/dist/config.js.map +1 -1
  84. package/dist/connection-secrets.d.ts +6 -0
  85. package/dist/connection-secrets.js +4 -0
  86. package/dist/connection-secrets.js.map +1 -1
  87. package/dist/daemon.d.ts +95 -4
  88. package/dist/daemon.js +748 -111
  89. package/dist/daemon.js.map +1 -1
  90. package/dist/darwin-memory.d.ts +6 -1
  91. package/dist/darwin-memory.js +17 -6
  92. package/dist/darwin-memory.js.map +1 -1
  93. package/dist/delivery-consumed-watch.d.ts +85 -0
  94. package/dist/delivery-consumed-watch.js +244 -0
  95. package/dist/delivery-consumed-watch.js.map +1 -0
  96. package/dist/delivery-outbox.d.ts +61 -0
  97. package/dist/delivery-outbox.js +120 -2
  98. package/dist/delivery-outbox.js.map +1 -1
  99. package/dist/delivery-reconciliation.d.ts +14 -5
  100. package/dist/delivery-reconciliation.js +27 -4
  101. package/dist/delivery-reconciliation.js.map +1 -1
  102. package/dist/delivery-reconciliation.test.js +35 -1
  103. package/dist/delivery-reconciliation.test.js.map +1 -1
  104. package/dist/event-log.d.ts +4 -1
  105. package/dist/event-log.js +6 -3
  106. package/dist/event-log.js.map +1 -1
  107. package/dist/event-loop-watch.d.ts +28 -0
  108. package/dist/event-loop-watch.js +75 -3
  109. package/dist/event-loop-watch.js.map +1 -1
  110. package/dist/fleet-context.d.ts +22 -2
  111. package/dist/fleet-control-audit.d.ts +5 -0
  112. package/dist/fleet-control-audit.js +7 -0
  113. package/dist/fleet-control-audit.js.map +1 -1
  114. package/dist/fleet-level-config.d.ts +2 -2
  115. package/dist/fleet-level-config.js +28 -1
  116. package/dist/fleet-level-config.js.map +1 -1
  117. package/dist/fleet-manager.d.ts +339 -12
  118. package/dist/fleet-manager.js +2623 -461
  119. package/dist/fleet-manager.js.map +1 -1
  120. package/dist/full-restart.js +2 -3
  121. package/dist/full-restart.js.map +1 -1
  122. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  123. package/dist/host-memory.d.ts +4 -0
  124. package/dist/host-memory.js.map +1 -1
  125. package/dist/install-lock.d.ts +49 -0
  126. package/dist/install-lock.js +200 -0
  127. package/dist/install-lock.js.map +1 -0
  128. package/dist/instance-config-impact.js +8 -2
  129. package/dist/instance-config-impact.js.map +1 -1
  130. package/dist/instance-lifecycle.d.ts +10 -5
  131. package/dist/instance-lifecycle.js +162 -53
  132. package/dist/instance-lifecycle.js.map +1 -1
  133. package/dist/locale.js +164 -16
  134. package/dist/locale.js.map +1 -1
  135. package/dist/login-controller.d.ts +2 -0
  136. package/dist/login-controller.js +17 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/memory-pressure.d.ts +7 -8
  139. package/dist/memory-pressure.js +76 -15
  140. package/dist/memory-pressure.js.map +1 -1
  141. package/dist/monotonic-budget.d.ts +2 -0
  142. package/dist/monotonic-budget.js +28 -0
  143. package/dist/monotonic-budget.js.map +1 -0
  144. package/dist/needs-you-hub.d.ts +114 -0
  145. package/dist/needs-you-hub.js +370 -0
  146. package/dist/needs-you-hub.js.map +1 -0
  147. package/dist/needs-you-live.d.ts +116 -0
  148. package/dist/needs-you-live.js +228 -0
  149. package/dist/needs-you-live.js.map +1 -0
  150. package/dist/needs-you.d.ts +132 -0
  151. package/dist/needs-you.js +164 -0
  152. package/dist/needs-you.js.map +1 -0
  153. package/dist/node-version-guard.d.ts +30 -0
  154. package/dist/node-version-guard.js +50 -0
  155. package/dist/node-version-guard.js.map +1 -0
  156. package/dist/operator-control-paths.d.ts +4 -0
  157. package/dist/operator-control-paths.js +24 -0
  158. package/dist/operator-control-paths.js.map +1 -0
  159. package/dist/outbound-handlers.d.ts +2 -0
  160. package/dist/outbound-handlers.js +9 -2
  161. package/dist/outbound-handlers.js.map +1 -1
  162. package/dist/outbound-schemas.d.ts +5 -5
  163. package/dist/outbound-schemas.js +1 -1
  164. package/dist/outbound-schemas.js.map +1 -1
  165. package/dist/package-preimage.d.ts +26 -0
  166. package/dist/package-preimage.js +96 -0
  167. package/dist/package-preimage.js.map +1 -0
  168. package/dist/profile-control.js +4 -20
  169. package/dist/profile-control.js.map +1 -1
  170. package/dist/public-web-gateway.d.ts +18 -0
  171. package/dist/public-web-gateway.js +141 -0
  172. package/dist/public-web-gateway.js.map +1 -0
  173. package/dist/public-web-link.d.ts +53 -0
  174. package/dist/public-web-link.js +175 -0
  175. package/dist/public-web-link.js.map +1 -0
  176. package/dist/quickstart-api.d.ts +8 -1
  177. package/dist/quickstart-api.js +86 -48
  178. package/dist/quickstart-api.js.map +1 -1
  179. package/dist/quickstart.d.ts +1 -0
  180. package/dist/quickstart.js +16 -6
  181. package/dist/quickstart.js.map +1 -1
  182. package/dist/restart-guard.d.ts +52 -0
  183. package/dist/restart-guard.js +109 -0
  184. package/dist/restart-guard.js.map +1 -0
  185. package/dist/scheduler/db.d.ts +7 -1
  186. package/dist/scheduler/db.js +44 -2
  187. package/dist/scheduler/db.js.map +1 -1
  188. package/dist/scheduler/index.d.ts +1 -1
  189. package/dist/scheduler/retry-label.d.ts +11 -0
  190. package/dist/scheduler/retry-label.js +24 -0
  191. package/dist/scheduler/retry-label.js.map +1 -0
  192. package/dist/scheduler/scheduler.d.ts +55 -2
  193. package/dist/scheduler/scheduler.js +220 -8
  194. package/dist/scheduler/scheduler.js.map +1 -1
  195. package/dist/scheduler/types.d.ts +22 -0
  196. package/dist/scheduler/types.js.map +1 -1
  197. package/dist/secret-store.d.ts +5 -0
  198. package/dist/secret-store.js +20 -0
  199. package/dist/secret-store.js.map +1 -1
  200. package/dist/service-activation.d.ts +152 -0
  201. package/dist/service-activation.js +407 -0
  202. package/dist/service-activation.js.map +1 -0
  203. package/dist/service-installer.d.ts +63 -1
  204. package/dist/service-installer.js +141 -16
  205. package/dist/service-installer.js.map +1 -1
  206. package/dist/service-plan.d.ts +65 -0
  207. package/dist/service-plan.js +108 -0
  208. package/dist/service-plan.js.map +1 -0
  209. package/dist/service-restart-selection.d.ts +6 -5
  210. package/dist/service-restart-selection.js +6 -5
  211. package/dist/service-restart-selection.js.map +1 -1
  212. package/dist/settings-api.d.ts +10 -2
  213. package/dist/settings-api.js +216 -72
  214. package/dist/settings-api.js.map +1 -1
  215. package/dist/settings-baseline.d.ts +20 -0
  216. package/dist/settings-baseline.js +104 -0
  217. package/dist/settings-baseline.js.map +1 -0
  218. package/dist/settings-change.d.ts +21 -0
  219. package/dist/settings-change.js +161 -0
  220. package/dist/settings-change.js.map +1 -0
  221. package/dist/settings-confirmation.d.ts +105 -0
  222. package/dist/settings-confirmation.js +232 -0
  223. package/dist/settings-confirmation.js.map +1 -0
  224. package/dist/settings-control.d.ts +41 -0
  225. package/dist/settings-control.js +294 -0
  226. package/dist/settings-control.js.map +1 -0
  227. package/dist/settings-effect.d.ts +24 -0
  228. package/dist/settings-effect.js +169 -0
  229. package/dist/settings-effect.js.map +1 -0
  230. package/dist/settings-http-confirmation.d.ts +29 -0
  231. package/dist/settings-http-confirmation.js +123 -0
  232. package/dist/settings-http-confirmation.js.map +1 -0
  233. package/dist/settings-instance-patch.d.ts +4 -0
  234. package/dist/settings-instance-patch.js +26 -0
  235. package/dist/settings-instance-patch.js.map +1 -0
  236. package/dist/settings-request-capability.d.ts +16 -0
  237. package/dist/settings-request-capability.js +32 -0
  238. package/dist/settings-request-capability.js.map +1 -0
  239. package/dist/settings-transaction.d.ts +65 -0
  240. package/dist/settings-transaction.js +223 -0
  241. package/dist/settings-transaction.js.map +1 -0
  242. package/dist/setup-auth.d.ts +10 -18
  243. package/dist/setup-auth.js +14 -38
  244. package/dist/setup-auth.js.map +1 -1
  245. package/dist/setup-form.d.ts +1 -1
  246. package/dist/setup-form.js +75 -18
  247. package/dist/setup-form.js.map +1 -1
  248. package/dist/setup-guide.d.ts +13 -0
  249. package/dist/setup-guide.js +25 -0
  250. package/dist/setup-guide.js.map +1 -0
  251. package/dist/setup-host.d.ts +16 -0
  252. package/dist/setup-host.js +199 -35
  253. package/dist/setup-host.js.map +1 -1
  254. package/dist/setup-wizard.js +5 -1
  255. package/dist/setup-wizard.js.map +1 -1
  256. package/dist/slash-authz.d.ts +17 -3
  257. package/dist/slash-authz.js +5 -0
  258. package/dist/slash-authz.js.map +1 -1
  259. package/dist/spawn-gate.js +3 -3
  260. package/dist/spawn-gate.js.map +1 -1
  261. package/dist/sqlite-open-errors.d.ts +12 -0
  262. package/dist/sqlite-open-errors.js +13 -0
  263. package/dist/sqlite-open-errors.js.map +1 -0
  264. package/dist/statusline-watcher.d.ts +5 -0
  265. package/dist/statusline-watcher.js +2 -0
  266. package/dist/statusline-watcher.js.map +1 -1
  267. package/dist/steer-capability.d.ts +6 -0
  268. package/dist/steer-capability.js +8 -0
  269. package/dist/steer-capability.js.map +1 -1
  270. package/dist/sync-work-attribution.d.ts +14 -0
  271. package/dist/sync-work-attribution.js +43 -8
  272. package/dist/sync-work-attribution.js.map +1 -1
  273. package/dist/tips.js +10 -5
  274. package/dist/tips.js.map +1 -1
  275. package/dist/tmux-control.d.ts +57 -5
  276. package/dist/tmux-control.js +379 -61
  277. package/dist/tmux-control.js.map +1 -1
  278. package/dist/tmux-manager.d.ts +20 -6
  279. package/dist/tmux-manager.js +90 -47
  280. package/dist/tmux-manager.js.map +1 -1
  281. package/dist/tmux-read.d.ts +59 -0
  282. package/dist/tmux-read.js +213 -0
  283. package/dist/tmux-read.js.map +1 -0
  284. package/dist/topic-commands.d.ts +21 -10
  285. package/dist/topic-commands.js +205 -63
  286. package/dist/topic-commands.js.map +1 -1
  287. package/dist/transcript-delta-reader.js +2 -1
  288. package/dist/transcript-delta-reader.js.map +1 -1
  289. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  290. package/dist/tunnel/cloudflared-install.js +1 -1
  291. package/dist/tunnel/cloudflared-install.js.map +1 -1
  292. package/dist/tunnel/purpose-lane.d.ts +14 -0
  293. package/dist/tunnel/purpose-lane.js +43 -0
  294. package/dist/tunnel/purpose-lane.js.map +1 -0
  295. package/dist/turn-reply-guard.d.ts +2 -0
  296. package/dist/turn-reply-guard.js +10 -1
  297. package/dist/turn-reply-guard.js.map +1 -1
  298. package/dist/types.d.ts +33 -0
  299. package/dist/ui/app.html +18 -0
  300. package/dist/ui/chat-commands.js +43 -0
  301. package/dist/ui/chat-render.js +478 -0
  302. package/dist/ui/chat-store.js +318 -0
  303. package/dist/ui/chat-strings.js +89 -0
  304. package/dist/ui/chat-thread.js +246 -0
  305. package/dist/ui/chat-tour.js +116 -0
  306. package/dist/ui/fleet-cache.js +168 -0
  307. package/dist/ui/fleet-org.js +142 -0
  308. package/dist/ui/panel-chat.js +504 -0
  309. package/dist/ui/panel-fleet.js +427 -0
  310. package/dist/ui/panel-needs.js +202 -0
  311. package/dist/ui/panel-settings.js +754 -0
  312. package/dist/ui/preview-panel.js +202 -0
  313. package/dist/ui/preview.js +270 -0
  314. package/dist/ui/settings-apply.js +239 -0
  315. package/dist/ui/settings-confirm.js +186 -0
  316. package/dist/ui/settings-dialogs.js +546 -0
  317. package/dist/ui/settings-model.js +277 -0
  318. package/dist/ui/settings-strings.js +654 -0
  319. package/dist/ui/settings-wizard.js +140 -0
  320. package/dist/ui/shared/agend-auth.js +93 -0
  321. package/dist/ui/shared/app-ctx.js +80 -0
  322. package/dist/ui/shared/app-html.js +9 -0
  323. package/dist/ui/shared/app-i18n.js +95 -0
  324. package/dist/ui/shared/app-nav.js +73 -0
  325. package/dist/ui/shared/app-needs.js +114 -0
  326. package/dist/ui/shared/app-route.js +73 -0
  327. package/dist/ui/shared/app-session.js +66 -0
  328. package/dist/ui/shared/app-shell.js +346 -0
  329. package/dist/ui/shared/app-store.js +51 -0
  330. package/dist/ui/shared/app-stream.js +201 -0
  331. package/dist/ui/shared/app.css +820 -0
  332. package/dist/ui/shared/app.js +88 -0
  333. package/dist/ui/shared/fonts/OFL.txt +92 -0
  334. package/dist/ui/shared/fonts/README.md +21 -0
  335. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  336. package/dist/ui/shared/htm.module.js +1 -0
  337. package/dist/ui/shared/panel-view.js +655 -0
  338. package/dist/ui/shared/preact-hooks.module.js +2 -0
  339. package/dist/ui/shared/preact.module.js +2 -0
  340. package/dist/ui/shared/signin.js +142 -0
  341. package/dist/ui/shared/theme.js +29 -0
  342. package/dist/ui/shared/tokens.css +153 -0
  343. package/dist/ui/shared/ui-confirm.js +43 -0
  344. package/dist/ui/shared/ui-dialog.js +45 -0
  345. package/dist/ui/shared/ui-icons.js +63 -0
  346. package/dist/ui/shared/ui-menu.js +39 -0
  347. package/dist/ui/shared/ui-states.js +20 -0
  348. package/dist/ui/shared/ui-toast.js +18 -0
  349. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  350. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  351. package/dist/ui/shared/vendor/vendor.json +39 -0
  352. package/dist/ui/shared/view-strings.js +309 -0
  353. package/dist/ui/signin.html +52 -0
  354. package/dist/update-check.d.ts +6 -2
  355. package/dist/update-check.js +12 -3
  356. package/dist/update-check.js.map +1 -1
  357. package/dist/update-dispatch.d.ts +28 -0
  358. package/dist/update-dispatch.js +73 -0
  359. package/dist/update-dispatch.js.map +1 -0
  360. package/dist/update-install.d.ts +129 -0
  361. package/dist/update-install.js +262 -0
  362. package/dist/update-install.js.map +1 -0
  363. package/dist/upgrade-notices.d.ts +17 -0
  364. package/dist/upgrade-notices.js +93 -0
  365. package/dist/upgrade-notices.js.map +1 -0
  366. package/dist/usage/statusline-usage.d.ts +2 -0
  367. package/dist/usage/statusline-usage.js +14 -10
  368. package/dist/usage/statusline-usage.js.map +1 -1
  369. package/dist/view-api.d.ts +6 -1
  370. package/dist/view-api.js +62 -51
  371. package/dist/view-api.js.map +1 -1
  372. package/dist/web-api.d.ts +85 -14
  373. package/dist/web-api.js +557 -77
  374. package/dist/web-api.js.map +1 -1
  375. package/dist/web-auth.d.ts +71 -27
  376. package/dist/web-auth.js +169 -69
  377. package/dist/web-auth.js.map +1 -1
  378. package/dist/web-body.d.ts +3 -0
  379. package/dist/web-body.js +55 -0
  380. package/dist/web-body.js.map +1 -0
  381. package/dist/web-channel-echo.d.ts +9 -0
  382. package/dist/web-channel-echo.js +55 -0
  383. package/dist/web-channel-echo.js.map +1 -0
  384. package/dist/web-chat-history.d.ts +125 -0
  385. package/dist/web-chat-history.js +184 -0
  386. package/dist/web-chat-history.js.map +1 -0
  387. package/dist/web-commands.d.ts +77 -0
  388. package/dist/web-commands.js +81 -0
  389. package/dist/web-commands.js.map +1 -0
  390. package/dist/web-continuation.d.ts +7 -0
  391. package/dist/web-continuation.js +22 -0
  392. package/dist/web-continuation.js.map +1 -0
  393. package/dist/web-host-guard.d.ts +34 -0
  394. package/dist/web-host-guard.js +57 -1
  395. package/dist/web-host-guard.js.map +1 -1
  396. package/dist/web-login.d.ts +72 -0
  397. package/dist/web-login.js +132 -0
  398. package/dist/web-login.js.map +1 -0
  399. package/dist/web-org.d.ts +55 -0
  400. package/dist/web-org.js +67 -0
  401. package/dist/web-org.js.map +1 -0
  402. package/dist/web-preview.d.ts +77 -0
  403. package/dist/web-preview.js +240 -0
  404. package/dist/web-preview.js.map +1 -0
  405. package/dist/web-request-context.d.ts +10 -0
  406. package/dist/web-request-context.js +13 -0
  407. package/dist/web-request-context.js.map +1 -0
  408. package/dist/web-session.d.ts +167 -0
  409. package/dist/web-session.js +389 -0
  410. package/dist/web-session.js.map +1 -0
  411. package/dist/web-shell-routes.d.ts +59 -0
  412. package/dist/web-shell-routes.js +96 -0
  413. package/dist/web-shell-routes.js.map +1 -0
  414. package/dist/web-upload.d.ts +183 -0
  415. package/dist/web-upload.js +468 -0
  416. package/dist/web-upload.js.map +1 -0
  417. package/dist/website-tips-renderer.d.ts +2 -2
  418. package/dist/website-tips-renderer.js +3 -3
  419. package/dist/website-tips-renderer.js.map +1 -1
  420. package/launcher/agend +110 -0
  421. package/launcher/agend-agent +110 -0
  422. package/launcher/agend-agent.cjs +4 -0
  423. package/launcher/agend.cjs +4 -0
  424. package/launcher/install-admission.cjs +105 -0
  425. package/launcher/launch.cjs +64 -0
  426. package/launcher/postinstall.cjs +114 -0
  427. package/launcher/run-in-process.cjs +13 -0
  428. package/launcher/runtime-platform.cjs +86 -0
  429. package/launcher/runtime-select.cjs +217 -0
  430. package/package.json +18 -7
  431. package/scripts/preinstall-guard.cjs +55 -0
  432. package/templates/launchd.plist.ejs +2 -2
  433. package/templates/systemd.service.ejs +1 -1
  434. package/dist/ui/dashboard.html +0 -774
  435. package/dist/ui/settings.html +0 -2165
  436. package/dist/ui/view.html +0 -1139
package/dist/web-api.js CHANGED
@@ -1,3 +1,17 @@
1
+ import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
2
+ import { readBoundedWebBody } from "./web-body.js";
3
+ import { gatewayRequestContext } from "./web-request-context.js";
4
+ import { permitWebContinuation } from "./web-continuation.js";
5
+ /**
6
+ * Web UI HTTP API handler.
7
+ * All /ui/* routes are handled here, extracted from fleet-manager.ts.
8
+ */
9
+ import { formatWebChannelEcho } from "./web-channel-echo.js";
10
+ import { t } from "./locale.js";
11
+ import { isSafeInstanceName } from "./web-shell-routes.js";
12
+ import { MAX_COMMAND_ARGS } from "./web-commands.js";
13
+ import { sendPanelHtml } from "./web-host-guard.js";
14
+ import { shellRoute } from "./web-shell-routes.js";
1
15
  import { readFileSync } from "node:fs";
2
16
  import { join, dirname } from "node:path";
3
17
  import { fileURLToPath } from "node:url";
@@ -5,8 +19,12 @@ import { execFileSync } from "node:child_process";
5
19
  import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
6
20
  import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js";
7
21
  import { z } from "zod";
8
- import { isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
22
+ import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
23
+ import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
24
+ import { attachmentDelivery, displayName, INLINE_MIME, isFileId, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
25
+ import { getAgendHome } from "./paths.js";
9
26
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
27
+ import { measureSyncWork } from "./sync-work-attribution.js";
10
28
  // ── Strict public-facing schemas ────────────────────────────────────────────
11
29
  // web-api endpoints must reject unknown fields so the dashboard cannot inject
12
30
  // internal-only flags that would reach handleCreate/scheduler/config writers.
@@ -51,16 +69,20 @@ const ConfigUpdateSchema = z.object({
51
69
  }).strict();
52
70
  const SendMessageSchema = z.object({
53
71
  instance: z.string().min(1).max(128),
54
- message: z.string().min(1).max(MAX_TEXT),
55
- }).strict();
72
+ message: z.string().max(MAX_TEXT),
73
+ /** Ids from POST /ui/upload, for this instance, not yet sent. */
74
+ attachments: z.array(z.string().regex(/^[0-9a-f]{32}$/)).max(UPLOAD_LIMITS.maxFiles).optional(),
75
+ }).strict().refine(v => v.message.trim().length > 0 || (v.attachments?.length ?? 0) > 0, { message: "a message or at least one file", path: ["message"] });
76
+ /** An instance name that is safe as one path segment (no separator, no NUL, not a dot name). */
77
+ const safeInstanceSegment = (name) => /^[^\\/\x00]+$/.test(name) && name !== "." && name !== "..";
56
78
  /**
57
79
  * Push a single SSE frame to every client. If a client throws (closed socket
58
80
  * after a network drop, etc.), evict it from the set and continue — without
59
81
  * the eviction the dead entry leaks forever, and without the try/catch a
60
82
  * single dead client breaks delivery to every client iterated after it.
61
83
  */
62
- export function broadcastSseEvent(clients, event, data, onError) {
63
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
84
+ export function broadcastSseEvent(clients, event, data, onError, id) {
85
+ const payload = sseFrame(event, data, id);
64
86
  const dead = [];
65
87
  for (const client of clients) {
66
88
  try {
@@ -79,6 +101,10 @@ export function broadcastSseEvent(clients, event, data, onError) {
79
101
  catch { /* socket already gone */ }
80
102
  }
81
103
  }
104
+ /** One SSE frame; `id` (a chat message's) lets a reconnecting EventSource say what it last saw. */
105
+ export function sseFrame(event, data, id) {
106
+ return `${id !== undefined ? `id: ${id}\n` : ""}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
107
+ }
82
108
  function parseOrReject(schema, data, res) {
83
109
  const r = schema.safeParse(data);
84
110
  if (!r.success) {
@@ -91,20 +117,11 @@ function parseOrReject(schema, data, res) {
91
117
  }
92
118
  const __filename = fileURLToPath(import.meta.url);
93
119
  const __dirname = dirname(__filename);
120
+ /** How often a stream is refreshed, and how often it re-checks that its session still stands. */
121
+ export const SSE_HEARTBEAT_MS = 10_000;
94
122
  /** Parse JSON body from request. */
95
- function parseBody(req) {
96
- return new Promise((resolve, reject) => {
97
- let body = "";
98
- req.on("data", (chunk) => { body += chunk.toString(); });
99
- req.on("end", () => {
100
- try {
101
- resolve(JSON.parse(body));
102
- }
103
- catch {
104
- reject(new Error("Invalid JSON"));
105
- }
106
- });
107
- });
123
+ async function parseBody(req) {
124
+ return JSON.parse((await readBoundedWebBody(req, 512 * 1024)).toString("utf8"));
108
125
  }
109
126
  /** Send JSON response. */
110
127
  function json(res, status, data) {
@@ -119,11 +136,22 @@ export function handleWebRequest(req, res, url, ctx) {
119
136
  const path = url.pathname;
120
137
  const method = req.method ?? "GET";
121
138
  // Auth check for all /ui routes. Defence in depth behind the fleet-manager
122
- // gate, which has already redeemed any `?token=` for a session cookie — so
123
- // this accepts the cookie and the header too, and an unset token closes the
124
- // panel instead of comparing null against a missing credential.
139
+ // gate: the same session cookie or header token, and an unset token closes
140
+ // the panel instead of comparing null against a missing credential.
125
141
  if (path.startsWith("/ui")) {
126
- if (!isWebRequestAuthorized(req, url, ctx.webToken)) {
142
+ const approved = isSettingsReplay(req) && method === "POST"
143
+ && (path === "/ui/config" || path === "/ui/instances" || /^\/ui\/instances\/[^/]+\/delete$/.test(path))
144
+ ? settingsRequestExecution(req) : undefined;
145
+ if (approved) {
146
+ try {
147
+ approved.assert();
148
+ }
149
+ catch {
150
+ json(res, 409, { error: "settings_execution_stale" });
151
+ return true;
152
+ }
153
+ }
154
+ else if (!isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: !isPassiveWebRead(method, path) })) {
127
155
  json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
128
156
  return true;
129
157
  }
@@ -132,16 +160,16 @@ export function handleWebRequest(req, res, url, ctx) {
132
160
  return false;
133
161
  }
134
162
  // ── Static files ───────────────────────────────────────
135
- if (method === "GET" && path === "/ui") {
136
- try {
137
- const html = readFileSync(join(__dirname, "ui", "dashboard.html"), "utf-8");
138
- res.setHeader("Content-Type", "text/html; charset=utf-8");
139
- res.writeHead(200);
140
- res.end(html);
141
- }
142
- catch {
143
- json(res, 500, { error: "dashboard.html not found" });
144
- }
163
+ // #1408: the app shell, for exactly the paths the classifier names (src/web-shell-routes.ts). One page for every
164
+ // route; the client router mounts the panel. A malformed instance name is a 400, an unknown one is the shell (the
165
+ // panel says "not found"), so the response never tells which names exist.
166
+ const shell = shellRoute(method, path);
167
+ if (shell?.kind === "malformed") {
168
+ json(res, 400, { error: "invalid instance name" });
169
+ return true;
170
+ }
171
+ if (shell?.kind === "shell") {
172
+ serveAppShell(req, res, ctx, "full");
145
173
  return true;
146
174
  }
147
175
  // Serve JS modules
@@ -190,22 +218,83 @@ export function handleWebRequest(req, res, url, ctx) {
190
218
  return true;
191
219
  }
192
220
  // ── SSE ────────────────────────────────────────────────
221
+ // The same data as the stream, over plain requests, for a path that cannot carry SSE (Cloudflare Quick
222
+ // Tunnels do not; a buffering proxy looks like a mute server). `after` is the same `<boot>-<id>` cursor
223
+ // as the stream's Last-Event-ID, so polling and the stream can take turns without a message twice or
224
+ // a gap — and a cursor from before a fleet restart gets the new process's backlog. No cursor yet (the page
225
+ // has not seen a single stream message): everything still retained, like a restart's backlog, so a message
226
+ // that arrived while the stream was silent is not skipped by the cursor this answer hands out (#1251
227
+ // review). The page keeps one entry per boot+id, so what it already loaded from /ui/history is not doubled.
228
+ if (method === "GET" && path === "/ui/poll") {
229
+ const history = ctx.webChatHistory;
230
+ const after = url.searchParams.get("after");
231
+ const fresh = after === null || after === ""; // a cursor that is not ours in shape still gets nothing
232
+ json(res, 200, {
233
+ status: ctx.getUiStatus(),
234
+ messages: history ? (fresh ? history.after(0) : history.replayFor(parseLastEventId(after))) : [],
235
+ // The ticks too, so polling never has to re-read a chat's history — that read would count as the person's
236
+ // activity; this poll does not (isPassiveWebRead).
237
+ deliveries: history ? history.deliveries() : [],
238
+ // And the fleet prompts open on the dashboard (C4): prompt events are stream-only too.
239
+ prompts: ctx.listWebPrompts?.() ?? [],
240
+ // #1386: "Needs you" rides the passive channels only — no endpoint of its own to poll.
241
+ needs: ctx.needsYouItems?.() ?? [],
242
+ cursor: history ? `${history.boot}-${history.lastId}` : null,
243
+ });
244
+ return true;
245
+ }
193
246
  if (method === "GET" && path === "/ui/events") {
247
+ if (gatewayRequestContext(req)) {
248
+ json(res, 404, { error: "Use polling on the public link" });
249
+ return true;
250
+ }
194
251
  res.writeHead(200, {
195
252
  "Content-Type": "text/event-stream",
196
253
  "Cache-Control": "no-cache",
197
254
  Connection: "keep-alive",
198
255
  });
199
256
  res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`);
257
+ // An EventSource that reconnects says what it last saw: send what it missed, before anything new.
258
+ const lastSeen = parseLastEventId(req.headers["last-event-id"]);
259
+ if (lastSeen !== null && ctx.webChatHistory) {
260
+ const history = ctx.webChatHistory;
261
+ for (const m of history.replayFor(lastSeen))
262
+ res.write(sseFrame("message", m, history.cursorOf(m)));
263
+ }
264
+ // Ticks change messages already sent and carry no cursor: every (re)connect gets where each one is now, so a
265
+ // report that fell into a gap is not lost until a reload (#1253 review). No id: it is not a message.
266
+ if (ctx.webChatHistory)
267
+ res.write(`event: deliveries\ndata: ${JSON.stringify(ctx.webChatHistory.deliveries())}\n\n`);
268
+ // The same for the fleet prompts (C4): one posted, answered or expired during a gap is caught up here.
269
+ if (ctx.listWebPrompts)
270
+ res.write(`event: prompts\ndata: ${JSON.stringify(ctx.listWebPrompts())}\n\n`);
271
+ // #1386: and "Needs you" as it is now; changes follow as `needs` events.
272
+ if (ctx.needsYouItems)
273
+ res.write(`event: needs\ndata: ${JSON.stringify({ items: ctx.needsYouItems() })}\n\n`);
200
274
  ctx.sseClients.add(res);
201
275
  const interval = setInterval(() => {
276
+ // A stream authorized once must not outlive the authorization. Re-checked
277
+ // without counting as activity, or an open tab would keep an idle session
278
+ // alive forever; a revoked, expired or rotated-away session ends here.
279
+ if (!isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false })) {
280
+ // Close the connection too, not just the response: an ended response leaves the socket idle in
281
+ // keep-alive, still held open by whoever's session just ended.
282
+ const socket = req.socket;
283
+ try {
284
+ res.end(() => socket?.destroy());
285
+ }
286
+ catch { /* already closed */ }
287
+ cleanup();
288
+ return;
289
+ }
202
290
  try {
203
- res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`);
291
+ // Every open dashboard tab, every 10 s: the status of every instance, serialized on the fleet's loop (#1235).
292
+ res.write(measureSyncWork("web.sseStatus", () => `event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`));
204
293
  }
205
294
  catch {
206
295
  cleanup();
207
296
  }
208
- }, 10_000);
297
+ }, ctx.sseHeartbeatMs ?? SSE_HEARTBEAT_MS);
209
298
  let cleanedUp = false;
210
299
  const cleanup = () => {
211
300
  if (cleanedUp)
@@ -214,20 +303,216 @@ export function handleWebRequest(req, res, url, ctx) {
214
303
  ctx.sseClients.delete(res);
215
304
  clearInterval(interval);
216
305
  };
217
- // `close` covers normal disconnects; `error` covers network resets that
218
- // never deliver a clean FIN. Without both, dead clients accumulate in
306
+ // The *response's* `close` is the one that means the stream is over: it fires when the connection
307
+ // goes away or the response is finished. The request's `close` means "the request has been read"
308
+ // — on newer Node that can be as soon as it is consumed, which would drop a live stream from
309
+ // sseClients and stop its session re-check while the response is still open. `error` covers
310
+ // network resets that never deliver a clean FIN. Without these, dead clients accumulate in
219
311
  // sseClients and the heartbeat interval keeps firing forever.
220
- req.on("close", cleanup);
312
+ res.on("close", cleanup);
221
313
  req.on("error", cleanup);
222
314
  res.on("error", cleanup);
223
315
  return true;
224
316
  }
317
+ // ── Chat history ───────────────────────────────────────
318
+ if (method === "GET" && path === "/ui/history") {
319
+ const instance = url.searchParams.get("instance") ?? "";
320
+ const limitRaw = url.searchParams.get("limit");
321
+ const limit = limitRaw === null ? 200 : Number(limitRaw);
322
+ if (instance.length === 0 || instance.length > 128 || !Number.isInteger(limit) || limit < 1 || limit > 500) {
323
+ json(res, 400, { error: "instance (1-128 chars) and limit (1-500) required" });
324
+ return true;
325
+ }
326
+ json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
327
+ return true;
328
+ }
225
329
  // ── Send message ───────────────────────────────────────
226
330
  if (method === "POST" && path === "/ui/send") {
227
331
  handleSendMessage(req, res, ctx);
228
332
  return true;
229
333
  }
334
+ // ── Files (C2): upload one file for an instance; fetch a file by the id the fleet issued for it ──
335
+ if (method === "POST" && path === "/ui/upload") {
336
+ handleUpload(req, res, url, ctx);
337
+ return true;
338
+ }
339
+ const fileMatch = path.match(/^\/ui\/file\/([^/]+)$/);
340
+ if (method === "GET" && fileMatch) {
341
+ const id = fileMatch[1];
342
+ const got = isFileId(id) ? ctx.webFiles?.read(id) : null;
343
+ if (!got) {
344
+ json(res, 404, { error: "No such file" });
345
+ return true;
346
+ }
347
+ const { file, bytes } = got;
348
+ const inline = INLINE_MIME.has(file.mime);
349
+ // Only the four image types are shown in the page; anything else is a download, never rendered.
350
+ res.setHeader("Content-Type", inline ? file.mime : (file.mime.startsWith("text/") ? "text/plain; charset=utf-8" : "application/octet-stream"));
351
+ res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
352
+ res.setHeader("X-Content-Type-Options", "nosniff");
353
+ res.setHeader("Cache-Control", "private, no-store");
354
+ res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
355
+ res.writeHead(200);
356
+ res.end(bytes);
357
+ return true;
358
+ }
359
+ // ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
360
+ if (method === "GET" && path === "/ui/prompts") {
361
+ json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
362
+ return true;
363
+ }
364
+ // The nonce is the capability, so it travels in the body: never in a URL a log or a history could keep.
365
+ if (method === "POST" && path === "/ui/prompt") {
366
+ if (!ctx.clickWebPrompt) {
367
+ json(res, 404, { error: "No prompts here" });
368
+ return true;
369
+ }
370
+ const click = ctx.clickWebPrompt.bind(ctx);
371
+ (async () => {
372
+ let body;
373
+ try {
374
+ body = await parseBody(req);
375
+ }
376
+ catch {
377
+ json(res, 400, { error: "Invalid JSON" });
378
+ return;
379
+ }
380
+ if (!permitWebContinuation(req, res, ctx))
381
+ return;
382
+ if (!body || typeof body !== "object") {
383
+ json(res, 400, { error: "instance, nonce and action required" });
384
+ return;
385
+ }
386
+ const { instance, nonce, action } = body;
387
+ if (typeof instance !== "string" || typeof nonce !== "string" || typeof action !== "string") {
388
+ json(res, 400, { error: "instance, nonce and action required" });
389
+ return;
390
+ }
391
+ const r = await click(instance, nonce, action);
392
+ // 409: answered elsewhere first, expired, or never open — the page drops the buttons on `gone`.
393
+ json(res, r.status, r.status === 200 ? { answered: true } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
394
+ })().catch(err => {
395
+ ctx.logger.error({ err: err.message }, "Web prompt answer failed");
396
+ try {
397
+ json(res, 500, { error: "Prompt answer failed" });
398
+ }
399
+ catch { /* already answered */ }
400
+ });
401
+ return true;
402
+ }
403
+ // #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
404
+ // token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
405
+ if (method === "POST" && path === "/ui/needs/ack") {
406
+ if (!ctx.acknowledgeNeedsItem) {
407
+ json(res, 404, { error: "Nothing to acknowledge here" });
408
+ return true;
409
+ }
410
+ const acknowledge = ctx.acknowledgeNeedsItem.bind(ctx);
411
+ (async () => {
412
+ let body;
413
+ try {
414
+ body = await parseBody(req);
415
+ }
416
+ catch {
417
+ json(res, 400, { error: "Invalid JSON" });
418
+ return;
419
+ }
420
+ if (!permitWebContinuation(req, res, ctx))
421
+ return;
422
+ const id = body && typeof body === "object" ? body.id : undefined;
423
+ if (typeof id !== "string") {
424
+ json(res, 400, { error: "id required" });
425
+ return;
426
+ }
427
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
428
+ if (verdict.kind !== "allow") {
429
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
430
+ return;
431
+ }
432
+ const principal = verdict.via === "session" && verdict.session ? `web:${verdict.session.handle}` : "cli";
433
+ const r = acknowledge(id, principal);
434
+ json(res, r.status, r.status === 200 ? { acknowledged: true, message: r.message } : { error: r.message });
435
+ })().catch(err => {
436
+ ctx.logger.error({ err: err.message }, "Needs you acknowledge failed");
437
+ try {
438
+ json(res, 500, { error: "Acknowledge failed" });
439
+ }
440
+ catch { /* already answered */ }
441
+ });
442
+ return true;
443
+ }
444
+ // #1269: an instance's chat command (/ctx, /compact, /clear, /model, …) — a session write (CSRF, checked by the
445
+ // gate), run through the platforms' own handlers and command table. The public link is told it is one (/save is
446
+ // refused there).
447
+ if (method === "POST" && path === "/ui/command") {
448
+ if (!ctx.webCommand) {
449
+ json(res, 404, { error: "Not available" });
450
+ return true;
451
+ }
452
+ const run = ctx.webCommand.bind(ctx);
453
+ (async () => {
454
+ let body;
455
+ try {
456
+ body = await parseBody(req);
457
+ }
458
+ catch {
459
+ json(res, 400, { error: "Invalid JSON" });
460
+ return;
461
+ }
462
+ if (!permitWebContinuation(req, res, ctx))
463
+ return;
464
+ const str = (v, max) => (typeof v === "string" && v.length <= max ? v : undefined);
465
+ const instance = str(body?.instance, 128), command = str(body?.command, 32);
466
+ if (!instance || !isSafeInstanceName(instance) || !command) {
467
+ json(res, 400, { error: "instance and command required" });
468
+ return;
469
+ }
470
+ if (body.args !== undefined && str(body.args, MAX_COMMAND_ARGS) === undefined) {
471
+ json(res, 400, { error: "args too long" });
472
+ return;
473
+ }
474
+ if (body.confirm !== undefined && !(typeof body.confirm === "string" && /^[0-9a-f]{32}$/.test(body.confirm))) {
475
+ json(res, 400, { error: "invalid confirmation" });
476
+ return;
477
+ }
478
+ const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req) });
479
+ json(res, r.status, r.body);
480
+ })().catch(err => {
481
+ ctx.logger.error({ err: err.message }, "Web command failed");
482
+ try {
483
+ json(res, 500, { error: "Command failed" });
484
+ }
485
+ catch { /* already answered */ }
486
+ });
487
+ return true;
488
+ }
230
489
  // ── Instance operations ────────────────────────────────
490
+ // Stop the agent's current reply — what Telegram's cancel button and /cancel do: Esc into the CLI, and the
491
+ // messages still waiting for it are dropped. Not /ui/stop, which stops the instance's process.
492
+ const cancelMatch = path.match(/^\/ui\/cancel\/([^/]+)$/);
493
+ if (method === "POST" && cancelMatch) {
494
+ let name;
495
+ try {
496
+ name = decodeURIComponent(cancelMatch[1]);
497
+ }
498
+ catch {
499
+ json(res, 400, { error: "Bad instance name" });
500
+ return true;
501
+ }
502
+ // Own keys only: "constructor" is in every object and names no instance.
503
+ const known = (ctx.fleetConfig ? Object.hasOwn(ctx.fleetConfig.instances, name) : false) || ctx.daemons.has(name);
504
+ if (!ctx.cancelInstance || !known) {
505
+ json(res, 404, { error: `Instance not found: ${name}` });
506
+ return true;
507
+ }
508
+ if (!ctx.cancelInstance(name)) {
509
+ json(res, 409, { error: `${name} is not running` });
510
+ return true;
511
+ }
512
+ ctx.eventLog?.logActivity("cancel", "web-user", "stop the current reply", name);
513
+ json(res, 200, { cancelled: name });
514
+ return true;
515
+ }
231
516
  const stopMatch = path.match(/^\/ui\/stop\/(.+)$/);
232
517
  if (method === "POST" && stopMatch) {
233
518
  const name = decodeURIComponent(stopMatch[1]);
@@ -278,11 +563,17 @@ export function handleWebRequest(req, res, url, ctx) {
278
563
  (async () => {
279
564
  try {
280
565
  const body = await parseBody(req);
566
+ if (!permitWebContinuation(req, res, ctx))
567
+ return;
281
568
  if (body.confirm !== `delete ${name}`) {
282
569
  json(res, 400, { error: `Confirmation required: { "confirm": "delete ${name}" }` });
283
570
  return;
284
571
  }
285
- await ctx.removeInstance(name, authorizeExplicitInstanceRemoval("dashboard-confirmed"));
572
+ const execution = settingsRequestExecution(req);
573
+ if (execution)
574
+ await ctx.removeInstance(name, authorizeExplicitInstanceRemoval("dashboard-confirmed"), execution);
575
+ else
576
+ await ctx.removeInstance(name, authorizeExplicitInstanceRemoval("dashboard-confirmed"));
286
577
  ctx.emitSseEvent("status", ctx.getUiStatus());
287
578
  json(res, 200, { deleted: name });
288
579
  }
@@ -364,6 +655,8 @@ export function handleWebRequest(req, res, url, ctx) {
364
655
  (async () => {
365
656
  try {
366
657
  const body = await parseBody(req);
658
+ if (!permitWebContinuation(req, res, ctx))
659
+ return;
367
660
  const v = validateArgs(CreateInstanceArgs, body, "create_instance");
368
661
  if (!v.ok) {
369
662
  json(res, 400, { error: v.error });
@@ -371,7 +664,12 @@ export function handleWebRequest(req, res, url, ctx) {
371
664
  }
372
665
  let result = null;
373
666
  let error;
374
- await ctx.lifecycle.handleCreate(v.data, (r, e) => { result = r; error = e; });
667
+ const respond = (r, e) => { result = r; error = e; };
668
+ const execution = settingsRequestExecution(req);
669
+ if (execution)
670
+ await ctx.lifecycle.handleCreate(v.data, respond, undefined, execution);
671
+ else
672
+ await ctx.lifecycle.handleCreate(v.data, respond);
375
673
  if (error) {
376
674
  json(res, 400, { error });
377
675
  }
@@ -404,6 +702,8 @@ export function handleWebRequest(req, res, url, ctx) {
404
702
  (async () => {
405
703
  try {
406
704
  const body = await parseBody(req);
705
+ if (!permitWebContinuation(req, res, ctx))
706
+ return;
407
707
  const parsed = parseOrReject(TaskCreateSchema, body, res);
408
708
  if (!parsed)
409
709
  return;
@@ -432,6 +732,8 @@ export function handleWebRequest(req, res, url, ctx) {
432
732
  (async () => {
433
733
  try {
434
734
  const body = await parseBody(req);
735
+ if (!permitWebContinuation(req, res, ctx))
736
+ return;
435
737
  const parsed = parseOrReject(TaskUpdateSchema, body, res);
436
738
  if (!parsed)
437
739
  return;
@@ -472,6 +774,8 @@ export function handleWebRequest(req, res, url, ctx) {
472
774
  (async () => {
473
775
  try {
474
776
  const body = await parseBody(req);
777
+ if (!permitWebContinuation(req, res, ctx))
778
+ return;
475
779
  const parsed = parseOrReject(ScheduleCreateSchema, body, res);
476
780
  if (!parsed)
477
781
  return;
@@ -499,7 +803,23 @@ export function handleWebRequest(req, res, url, ctx) {
499
803
  }
500
804
  return true;
501
805
  }
806
+ // #1468: the prompt-cache expiry analysis. Opening it is a person's navigation, like any Fleet tab (it counts as
807
+ // use); the page never re-reads it on a timer. Answered from the ledgers as they are; a catch-up runs behind.
808
+ if (method === "GET" && path === "/ui/cache") {
809
+ if (!ctx.cacheReport) {
810
+ json(res, 404, { error: "Not available" });
811
+ return true;
812
+ }
813
+ ctx.cacheReport(url.searchParams.get("window") ?? "7d").then((report) => json(res, 200, report), (err) => { ctx.logger.error({ err }, "cache analysis failed"); json(res, 500, { error: "Cache analysis failed" }); });
814
+ return true;
815
+ }
502
816
  // ── Teams ──────────────────────────────────────────────
817
+ // #1389: the org chart. Opening it is a person's navigation (it counts as use, like a Fleet tab); nothing re-reads
818
+ // it on a timer — the live state comes over the stream the page already has.
819
+ if (method === "GET" && path === "/ui/org") {
820
+ json(res, 200, ctx.orgChart?.() ?? { general: [], teams: [], instances: {} });
821
+ return true;
822
+ }
503
823
  if (method === "GET" && path === "/ui/teams") {
504
824
  const teams = ctx.fleetConfig?.teams ?? {};
505
825
  json(res, 200, { teams });
@@ -509,6 +829,8 @@ export function handleWebRequest(req, res, url, ctx) {
509
829
  (async () => {
510
830
  try {
511
831
  const body = await parseBody(req);
832
+ if (!permitWebContinuation(req, res, ctx))
833
+ return;
512
834
  const parsed = parseOrReject(TeamCreateSchema, body, res);
513
835
  if (!parsed)
514
836
  return;
@@ -569,6 +891,8 @@ export function handleWebRequest(req, res, url, ctx) {
569
891
  (async () => {
570
892
  try {
571
893
  const body = await parseBody(req);
894
+ if (!permitWebContinuation(req, res, ctx))
895
+ return;
572
896
  const parsed = parseOrReject(ConfigUpdateSchema, body, res);
573
897
  if (!parsed)
574
898
  return;
@@ -578,26 +902,28 @@ export function handleWebRequest(req, res, url, ctx) {
578
902
  return;
579
903
  }
580
904
  const ch = config.channel;
581
- // Update channel settings
582
- if (parsed.channel && ch) {
583
- if (parsed.channel.group_id != null)
584
- config.channel.group_id = parsed.channel.group_id;
585
- if (parsed.channel.access)
586
- config.channel.access = parsed.channel.access;
587
- }
588
- // Update defaults
589
- if (parsed.defaults) {
590
- const d = config.defaults;
591
- if (parsed.defaults.backend)
592
- d.backend = parsed.defaults.backend;
593
- if (parsed.defaults.model)
594
- d.model = parsed.defaults.model;
595
- }
596
- // Update project_roots
597
- if (parsed.project_roots) {
598
- config.project_roots = parsed.project_roots;
599
- }
600
- ctx.saveFleetConfig();
905
+ settingsWrite(req, () => {
906
+ // Update channel settings
907
+ if (parsed.channel && ch) {
908
+ if (parsed.channel.group_id != null)
909
+ config.channel.group_id = parsed.channel.group_id;
910
+ if (parsed.channel.access)
911
+ config.channel.access = parsed.channel.access;
912
+ }
913
+ // Update defaults
914
+ if (parsed.defaults) {
915
+ const d = config.defaults;
916
+ if (parsed.defaults.backend)
917
+ d.backend = parsed.defaults.backend;
918
+ if (parsed.defaults.model)
919
+ d.model = parsed.defaults.model;
920
+ }
921
+ // Update project_roots
922
+ if (parsed.project_roots) {
923
+ config.project_roots = parsed.project_roots;
924
+ }
925
+ ctx.saveFleetConfig();
926
+ });
601
927
  const needsRestart = parsed.channel?.group_id != null;
602
928
  json(res, 200, { saved: true, needs_restart: needsRestart });
603
929
  }
@@ -611,6 +937,14 @@ export function handleWebRequest(req, res, url, ctx) {
611
937
  json(res, 404, { error: "not found" });
612
938
  return true;
613
939
  }
940
+ /** Keep the echo in a single platform message, including its attachment names. */
941
+ function webEchoPreview(message, names) {
942
+ const text = wellFormed(message.slice(0, 500));
943
+ const files = names.length ? `[📎 ${names.map(name => displayName(name, "attachment")).join(", ")}]` : "";
944
+ const preview = [text, files].filter(Boolean).join(" ");
945
+ const truncated = message.length > 500 || preview.length > 1400;
946
+ return wellFormed(preview.slice(0, 1400)) + (truncated ? ` … (${t("web.echo_full_text")})` : "");
947
+ }
614
948
  /** Handle POST /ui/send — extracted for readability. */
615
949
  function handleSendMessage(req, res, ctx) {
616
950
  let body = "";
@@ -625,6 +959,8 @@ function handleSendMessage(req, res, ctx) {
625
959
  body += chunk.toString();
626
960
  });
627
961
  req.on("end", async () => {
962
+ if (!permitWebContinuation(req, res, ctx))
963
+ return;
628
964
  try {
629
965
  const raw = JSON.parse(body);
630
966
  const parsed = SendMessageSchema.safeParse(raw);
@@ -633,55 +969,199 @@ function handleSendMessage(req, res, ctx) {
633
969
  json(res, 400, { error: `${issue.path.join(".") || "body"}: ${issue.message}` });
634
970
  return;
635
971
  }
636
- const { instance, message } = parsed.data;
972
+ const { instance, message: typed, attachments = [] } = parsed.data;
637
973
  const ipc = ctx.instanceIpcClients.get(instance);
638
974
  if (!ipc) {
639
975
  json(res, 404, { error: `Instance not found: ${instance}` });
640
976
  return;
641
977
  }
978
+ let files = [];
979
+ if (attachments.length > 0) {
980
+ const taken = ctx.webFiles?.takeForMessage(instance, attachments);
981
+ if (!taken || !taken.ok) {
982
+ json(res, 400, { error: taken && !taken.ok ? taken.error : "files are not available here" });
983
+ return;
984
+ }
985
+ files = taken.entries;
986
+ }
987
+ // The same tags and meta a Telegram photo/document produces, so the agent needs nothing new.
988
+ const delivery = attachmentDelivery(typed, files);
989
+ // #1269: `/raw ` is pasted into the CLI with no [user:] envelope (the daemon's own check, on this exact text).
990
+ // A signed-in local session may do that, as a fleet admin on the platform may; the public link — the outward-
991
+ // facing surface — may not.
992
+ if (gatewayRequestContext(req) && delivery.text.startsWith("/raw ")) {
993
+ ctx.webFiles?.release(files);
994
+ json(res, 403, { error: t("web.raw_public_refused") });
995
+ return;
996
+ }
997
+ const message = typed;
642
998
  const ts = new Date().toISOString();
999
+ // The id the agent is given, and the one its delivery reports come back under: the page's ticks.
1000
+ const messageId = newWebMessageId();
643
1001
  // Use real Telegram context so daemon's lastChatId/lastThreadId are set,
644
1002
  // enabling reply tool even when first message comes from Web UI.
645
- // Pure Web UI mode (no channel config) leaves these empty — TODO: needs
646
- // a separate reply path for that case.
647
- const groupId = ctx.fleetConfig?.channel?.group_id;
1003
+ // Pure Web UI mode (no channel config) leaves these empty; the agent's reply then comes back to the
1004
+ // web chat alone (FleetManager's web-only reply sink).
1005
+ const syncAdapter = ctx.getAdapterForInstance ? ctx.getAdapterForInstance(instance) : ctx.adapter;
1006
+ const groupId = ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "");
648
1007
  const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
1008
+ const canEcho = ctx.fleetConfig?.web?.echo_to_channel !== false
1009
+ && !ctx.isClassicInstance?.(instance) && syncAdapter && groupId && topicId != null;
1010
+ // Add no runtime credentials or local attachment paths to the display copy.
1011
+ const preview = webEchoPreview(message, files.map(f => f.name));
1012
+ // ClassicBot echo (#1320 part B): same ordering lane, per-entry
1013
+ // opt-in resolved at send time inside sendClassicWebEcho.
1014
+ const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
1015
+ const settleClassicEcho = canClassicEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
1016
+ await ctx.sendClassicWebEcho(instance, formatWebChannelEcho("web-user", preview, t("web.echo_full_text")));
1017
+ }) : undefined;
1018
+ const settleEcho = canEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
1019
+ // A replacement adapter or edited binding is not the route we reserved.
1020
+ if ((ctx.getAdapterForInstance ? ctx.getAdapterForInstance(instance) : ctx.adapter) !== syncAdapter
1021
+ || (ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "")) !== groupId
1022
+ || ctx.fleetConfig?.instances[instance]?.topic_id !== topicId)
1023
+ return;
1024
+ return syncAdapter.sendText(String(groupId), formatWebChannelEcho("web-user", preview, t("web.echo_full_text")), { threadId: String(topicId), format: "text", allowedMentions: { parse: [] } });
1025
+ }) : undefined;
1026
+ let delivered;
649
1027
  try {
650
- await ctx.deliverToInstance(instance, {
1028
+ delivered = await ctx.deliverToInstance(instance, {
651
1029
  type: "fleet_inbound",
652
- content: message,
1030
+ content: delivery.text,
653
1031
  targetSession: instance,
654
1032
  meta: {
655
1033
  chat_id: groupId ? String(groupId) : "",
656
- message_id: `web-${Date.now()}`,
1034
+ message_id: messageId,
657
1035
  user: "web-user", user_id: "web-user",
658
1036
  ts,
659
1037
  thread_id: topicId != null ? String(topicId) : "",
660
1038
  source: "web",
1039
+ adapter_id: syncAdapter?.id,
1040
+ ...delivery.meta,
661
1041
  },
662
1042
  });
663
1043
  }
664
1044
  catch (err) {
1045
+ settleEcho?.(false);
1046
+ // A throw must release the Classic ordering lane too: otherwise the
1047
+ // next accepted echo queues behind a reservation that never settles
1048
+ // and is dropped by expiry (#1330 R2).
1049
+ settleClassicEcho?.(false);
1050
+ ctx.webFiles?.release(files); // not delivered: the same ids can be sent again
665
1051
  ctx.logger.error({ err, instance }, "Web message delivery failed");
666
1052
  json(res, 503, { error: "Instance delivery failed" });
667
1053
  return;
668
1054
  }
1055
+ if (delivered === false) {
1056
+ settleEcho?.(false);
1057
+ settleClassicEcho?.(false);
1058
+ ctx.webFiles?.release(files);
1059
+ ctx.logger.error({ instance }, "Web message not delivered (the instance went away or restarted)");
1060
+ json(res, 503, { error: "Instance delivery failed" });
1061
+ return;
1062
+ }
1063
+ settleEcho?.(true);
1064
+ settleClassicEcho?.(true);
1065
+ ctx.webFiles?.commit(files);
669
1066
  ctx.lastInboundUser.set(instance, "web-user");
670
- ctx.eventLog?.logActivity("message", "web-user", message.slice(0, 200), instance);
671
- ctx.emitSseEvent("message", { instance, sender: "web-user", text: message, ts });
672
- // Sync to Telegram/Discord
673
- const syncAdapter = ctx.getAdapterForInstance?.(instance) ?? ctx.adapter;
674
- const syncGroupId = ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "");
675
- if (syncAdapter && syncGroupId) {
676
- const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
677
- const preview = message.length > 500 ? message.slice(0, 500) + " [...]" : message;
678
- syncAdapter.sendText(syncGroupId, `🌐 web-user: ${preview}`, { threadId: topicId != null ? String(topicId) : undefined }).catch(() => ctx.logger.debug({}, "Web→Channel sync failed"));
679
- }
680
- json(res, 200, { sent: true });
1067
+ ctx.eventLog?.logActivity("message", "web-user", (message || `[${files.length} file(s)]`).slice(0, 200), instance);
1068
+ ctx.emitSseEvent("message", { instance, sender: "web-user", role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId });
1069
+ json(res, 200, { sent: true, messageId });
681
1070
  }
682
1071
  catch {
683
1072
  json(res, 400, { error: "Invalid JSON" });
684
1073
  }
685
1074
  });
686
1075
  }
1076
+ /**
1077
+ * One file for one instance's chat. The body is the file itself (Content-Type is ignored: the type is
1078
+ * read from the bytes), `X-Agend-Filename` its name for the label only. Stored in the instance's
1079
+ * workspace inbox under a name chosen here; answered with the id the message will name.
1080
+ */
1081
+ function handleUpload(req, res, url, ctx) {
1082
+ const instance = url.searchParams.get("instance") ?? "";
1083
+ if (!ctx.webFiles) {
1084
+ json(res, 404, { error: "uploads are not available" });
1085
+ return;
1086
+ }
1087
+ if (!instance || instance.length > 128 || !safeInstanceSegment(instance) || !ctx.instanceIpcClients.has(instance)) {
1088
+ json(res, 404, { error: `Instance not found: ${instance}` });
1089
+ return;
1090
+ }
1091
+ const declared = Number(req.headers["content-length"] ?? NaN);
1092
+ if (Number.isFinite(declared) && declared > UPLOAD_LIMITS.maxFileBytes) {
1093
+ json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
1094
+ req.resume();
1095
+ return;
1096
+ }
1097
+ const chunks = [];
1098
+ let size = 0;
1099
+ let refused = false;
1100
+ req.on("data", (c) => {
1101
+ if (refused)
1102
+ return;
1103
+ size += c.length;
1104
+ if (size > UPLOAD_LIMITS.maxFileBytes) {
1105
+ refused = true;
1106
+ json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
1107
+ req.destroy();
1108
+ return;
1109
+ }
1110
+ chunks.push(c);
1111
+ });
1112
+ req.on("error", () => { });
1113
+ req.on("end", () => {
1114
+ if (!permitWebContinuation(req, res, ctx))
1115
+ return;
1116
+ if (refused)
1117
+ return;
1118
+ const bytes = Buffer.concat(chunks);
1119
+ const name = displayName(typeof req.headers["x-agend-filename"] === "string" ? req.headers["x-agend-filename"] : "", "file");
1120
+ const type = sniffUpload(bytes, name);
1121
+ if (!type) {
1122
+ json(res, 415, { error: "unsupported file type — images (PNG, JPEG, GIF, WebP), PDF and text files only" });
1123
+ return;
1124
+ }
1125
+ // The instance is checked again now the whole body is here: it may have been deleted (and its files
1126
+ // forgotten) or stopped while the upload streamed in; storing now would bring its id back (#1252 review).
1127
+ // Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
1128
+ // in fleet.yaml, or as a ClassicBot room in classicBot.yaml.
1129
+ const registered = !!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true;
1130
+ if (!ctx.instanceIpcClients.has(instance) || !registered) {
1131
+ json(res, 404, { error: `Instance not found: ${instance}` });
1132
+ return;
1133
+ }
1134
+ try {
1135
+ const entry = ctx.webFiles.storeUpload({ instance, inboxDir: join(getAgendHome(), "workspaces", instance, "inbox"), bytes, name, type });
1136
+ json(res, 200, publicAttachment(entry));
1137
+ }
1138
+ catch (err) {
1139
+ ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
1140
+ json(res, 500, { error: "the file could not be stored" });
1141
+ }
1142
+ });
1143
+ }
1144
+ function shellPreview(req, ctx, mode) {
1145
+ if (mode !== "full" || gatewayRequestContext(req))
1146
+ return null;
1147
+ return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
1148
+ }
1149
+ function shellBodyTag(req, ctx, mode) {
1150
+ const p = shellPreview(req, ctx, mode);
1151
+ const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
1152
+ 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)}"`
1153
+ + ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}">`;
1154
+ }
1155
+ /** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
1156
+ * when this load chose one. */
1157
+ export function serveAppShell(req, res, ctx, mode) {
1158
+ try {
1159
+ const html = readFileSync(join(__dirname, "ui", "app.html"), "utf-8");
1160
+ const p = shellPreview(req, ctx, mode);
1161
+ sendPanelHtml(res, html.replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, p?.previewOrigin ? { frameSrc: `${p.previewOrigin}/frame` } : {});
1162
+ }
1163
+ catch {
1164
+ json(res, 500, { error: "app.html not found" });
1165
+ }
1166
+ }
687
1167
  //# sourceMappingURL=web-api.js.map