@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
@@ -1,7 +1,41 @@
1
+ import { normalizeSettingsInstancePatch, removesInstanceOverride } from "./settings-instance-patch.js";
2
+ import { settingsRequestExecution, settingsWrite } from "./settings-request-capability.js";
3
+ import { noteSettingsWrite, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
4
+ import { readBoundedWebBody } from "./web-body.js";
5
+ import { validPublicLinkPatch } from "./public-web-link.js";
6
+ import { permitWebContinuation } from "./web-continuation.js";
7
+ /**
8
+ * Settings Web API (`/settings`) — CRUD over fleet.yaml + classicBot.yaml.
9
+ *
10
+ * GET /settings → static page
11
+ * GET /api/settings/fleet → fleet config as JSON
12
+ * GET /api/settings/classic → classicBot.yaml as JSON
13
+ * PUT /api/settings/fleet/defaults → merge fleet defaults
14
+ * PUT /api/settings/fleet/channels → replace channels[]
15
+ * POST /api/settings/fleet/instances/:name → create instance
16
+ * PATCH/api/settings/fleet/instances/:name → merge into instance
17
+ * DELETE /api/settings/fleet/instances/:name → remove instance
18
+ * PUT /api/settings/classic/defaults → merge classic defaults
19
+ * PATCH/api/settings/classic/channels/:key → update + restart a Classic channel
20
+ * POST /api/settings/reload → SIGHUP hot-reload
21
+ * POST /api/settings/instances/:name/pause → manually pause a running instance
22
+ * POST /api/settings/instances/:name/wake → manually wake a paused instance
23
+ * GET /api/settings/status-emojis → status-emoji keys, built-ins, pickable sets
24
+ * POST /api/settings/status-emojis/preview → resolve a map exactly as the bots will
25
+ * GET /api/settings/status-emojis/guild-emojis?channel=<id>[&refresh=1] → a Discord bot's servers' emojis
26
+ *
27
+ * Auth: all routes require the web.token — enforced by the global web-token gate
28
+ * in fleet-manager BEFORE this handler runs (settings paths are not exempt), so
29
+ * no per-route auth is repeated here.
30
+ *
31
+ * Writes are validated first (config-validator): any error → 400 and nothing is
32
+ * written; warnings are non-blocking and returned alongside the result.
33
+ */
34
+ import { serveAppShell } from "./web-api.js";
35
+ import { isSettingsPage } from "./web-shell-routes.js";
1
36
  import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, statSync } from "node:fs";
2
37
  import { randomBytes } from "node:crypto";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
38
+ import { join } from "node:path";
5
39
  import yaml from "js-yaml";
6
40
  import { KNOWN_BACKENDS, validateFleetConfig, validateClassicBotConfig } from "./config-validator.js";
7
41
  import { clearPausedMarker } from "./pause-marker.js";
@@ -11,7 +45,6 @@ import { handleQuickstartRequest } from "./quickstart-api.js";
11
45
  import { requestSessionBinding, } from "./connection-secrets.js";
12
46
  import { providerRegistryEnvKeys, isReservedProviderEnvKey } from "./provider-secret-registry.js";
13
47
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, previewStatusEmojis, } from "./status-emojis.js";
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
48
  function json(res, code, body, noStore = false) {
16
49
  res.writeHead(code, {
17
50
  "Content-Type": "application/json",
@@ -20,21 +53,7 @@ function json(res, code, body, noStore = false) {
20
53
  res.end(JSON.stringify(body));
21
54
  }
22
55
  function readBody(req, maxBytes) {
23
- return new Promise((resolve, reject) => {
24
- const chunks = [];
25
- let size = 0;
26
- req.on("data", (c) => {
27
- size += c.length;
28
- if (size > maxBytes) {
29
- reject(new Error("payload too large"));
30
- req.destroy();
31
- return;
32
- }
33
- chunks.push(c);
34
- });
35
- req.on("end", () => resolve(Buffer.concat(chunks)));
36
- req.on("error", reject);
37
- });
56
+ return readBoundedWebBody(req, maxBytes);
38
57
  }
39
58
  /**
40
59
  * Channel/group identifiers are opaque provider coordinates. JSON numbers
@@ -52,7 +71,7 @@ function normalizeChannelIdValue(value, path) {
52
71
  return { ok: false, error: `${path} must be a string (or a safe integer)` };
53
72
  }
54
73
  export function isSettingsPath(path) {
55
- return path === "/settings" || path.startsWith("/api/settings/");
74
+ return isSettingsPage(path) || path.startsWith("/api/settings/");
56
75
  }
57
76
  const classicPath = (ctx) => join(ctx.dataDir, "classicBot.yaml");
58
77
  class ClassicConfigParseError extends Error {
@@ -78,6 +97,7 @@ function writeClassicAtomic(ctx, classic) {
78
97
  const target = classicPath(ctx);
79
98
  const temp = `${target}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
80
99
  const mode = existsSync(target) ? statSync(target).mode : 0o600;
100
+ noteSettingsWrite(target, readClassic(ctx), classic);
81
101
  try {
82
102
  writeFileSync(temp, yaml.dump(classic, { lineWidth: -1 }), { encoding: "utf-8", mode });
83
103
  renameSync(temp, target);
@@ -98,6 +118,31 @@ const issueKey = (i) => i.path + "\u0000" + i.message;
98
118
  * user out of saving anything, which reads as "save didn't persist".
99
119
  * Returns true (and responds 400) when the edit adds errors.
100
120
  */
121
+ /**
122
+ * #1490 (Fable's 2.2 audit): the connection fields Settings writes. A PUT carries whole connections back, so a field
123
+ * outside these (one added by hand to fleet.yaml) passes through unchanged — but it is never added or changed here:
124
+ * an inline `bot_token`, or anything else, cannot reach fleet.yaml through this endpoint.
125
+ */
126
+ const CHANNEL_FIELDS = new Set(["id", "type", "mode", "bot_token_env", "group_id", "access", "options", "telegram_api_root", "mirror_topic_id"]);
127
+ const CHANNEL_ACCESS_FIELDS = new Set(["mode", "allowed_users", "max_pending_codes", "code_expiry_minutes"]);
128
+ const CHANNEL_OPTION_FIELDS = new Set(["category_name", "general_channel_id", "topic_probe", "sticker_sets", "status_emojis"]);
129
+ function unownedChannelField(candidate, previous) {
130
+ const record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
131
+ const check = (fields, next, prev, at) => {
132
+ const before = record(prev) ? prev : {};
133
+ for (const key of Object.keys(next)) {
134
+ if (fields.has(key))
135
+ continue;
136
+ if (Object.hasOwn(before, key) && JSON.stringify(before[key]) === JSON.stringify(next[key]))
137
+ continue;
138
+ return `${at}${key}`;
139
+ }
140
+ return null;
141
+ };
142
+ return check(CHANNEL_FIELDS, candidate, previous, "")
143
+ ?? (record(candidate.access) ? check(CHANNEL_ACCESS_FIELDS, candidate.access, previous?.access, "access.") : null)
144
+ ?? (record(candidate.options) ? check(CHANNEL_OPTION_FIELDS, candidate.options, previous?.options, "options.") : null);
145
+ }
101
146
  function rejectIfWorse(res, before, after) {
102
147
  const had = new Set(before.errors.map(issueKey));
103
148
  const introduced = after.errors.filter(e => !had.has(issueKey(e)));
@@ -118,16 +163,13 @@ export function handleSettingsRequest(req, res, url, ctx) {
118
163
  if (!isSettingsPath(path))
119
164
  return false;
120
165
  const method = req.method ?? "GET";
121
- // ── Static page ──
122
- if (method === "GET" && path === "/settings") {
123
- try {
124
- const html = readFileSync(join(__dirname, "ui", "settings.html"), "utf-8");
125
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
126
- res.end(html);
127
- }
128
- catch {
129
- json(res, 500, { error: "settings.html not found" });
166
+ // ── The page: the app shell, which mounts the Settings panel (#1408 step 3) ──
167
+ if (isSettingsPage(path)) {
168
+ if (method !== "GET" && method !== "HEAD") {
169
+ json(res, 405, { error: "Method not allowed" });
170
+ return true;
130
171
  }
172
+ serveAppShell(req, res, ctx, "full");
131
173
  return true;
132
174
  }
133
175
  // ── Reads ──
@@ -140,6 +182,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
140
182
  // Non-null means a restart cannot clear the fleet row, so the page shows
141
183
  // the mismatch instead of offering a button that can never succeed.
142
184
  fleet_signature_mismatch: ctx.fleetSignatureMismatchKeys?.() ?? null,
185
+ // Whether the provider-secret routes exist on this fleet: the page reads them only then (no 404 on every load).
186
+ provider_secrets: ctx.providerSecretsEnabled?.() === true,
143
187
  });
144
188
  return true;
145
189
  }
@@ -174,6 +218,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
174
218
  }
175
219
  if (method === "POST" && path === "/api/settings/status-emojis/preview") {
176
220
  readBody(req, 64 * 1024).then(buf => {
221
+ if (!permitWebContinuation(req, res, ctx))
222
+ return;
177
223
  let body;
178
224
  try {
179
225
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -279,6 +325,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
279
325
  }
280
326
  const specId = decodeURIComponent(providerSecretVerifyMatch[1]);
281
327
  readBody(req, 16 * 1024).then(async (buf) => {
328
+ if (!permitWebContinuation(req, res, ctx))
329
+ return;
282
330
  let body;
283
331
  try {
284
332
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -314,6 +362,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
314
362
  }
315
363
  const specId = decodeURIComponent(providerSecretApplyMatch[1]);
316
364
  readBody(req, 16 * 1024).then(buf => {
365
+ if (!permitWebContinuation(req, res, ctx))
366
+ return;
317
367
  let body;
318
368
  try {
319
369
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -328,7 +378,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
328
378
  return json(res, 400, { error: "verification_id required" }, true);
329
379
  if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
330
380
  return json(res, 400, { error: "Idempotency-Key required" }, true);
331
- const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key });
381
+ const result = ctx.startProviderSecretApply({ specId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req) });
332
382
  if ("error" in result)
333
383
  return json(res, 422, { ok: false, error: "provider secret apply rejected" }, true);
334
384
  if ("busy" in result)
@@ -364,6 +414,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
364
414
  }
365
415
  const connectionId = decodeURIComponent(secretVerifyMatch[1]);
366
416
  readBody(req, 16 * 1024).then(async (buf) => {
417
+ if (!permitWebContinuation(req, res, ctx))
418
+ return;
367
419
  let body;
368
420
  try {
369
421
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -407,6 +459,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
407
459
  }
408
460
  const connectionId = decodeURIComponent(secretApplyMatch[1]);
409
461
  readBody(req, 16 * 1024).then(buf => {
462
+ if (!permitWebContinuation(req, res, ctx))
463
+ return;
410
464
  let body;
411
465
  try {
412
466
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -425,7 +479,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
425
479
  connectionId,
426
480
  verificationId,
427
481
  sessionBinding: requestSessionBinding(req),
428
- idempotencyKey: key,
482
+ idempotencyKey: key, execution: settingsRequestExecution(req),
429
483
  });
430
484
  if ("error" in result)
431
485
  return json(res, 422, { ok: false, error: "secret apply rejected" }, true);
@@ -468,6 +522,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
468
522
  }
469
523
  const connectionId = decodeURIComponent(bindingVerifyMatch[1]);
470
524
  readBody(req, 16 * 1024).then(async (buf) => {
525
+ if (!permitWebContinuation(req, res, ctx))
526
+ return;
471
527
  let body;
472
528
  try {
473
529
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -511,6 +567,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
511
567
  }
512
568
  const connectionId = decodeURIComponent(bindingApplyMatch[1]);
513
569
  readBody(req, 16 * 1024).then(buf => {
570
+ if (!permitWebContinuation(req, res, ctx))
571
+ return;
514
572
  let body;
515
573
  try {
516
574
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -526,7 +584,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
526
584
  if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(key))
527
585
  return json(res, 400, { error: "Idempotency-Key required" }, true);
528
586
  const result = ctx.startConnectionBindingApply({
529
- connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key,
587
+ connectionId, verificationId, sessionBinding: requestSessionBinding(req), idempotencyKey: key, execution: settingsRequestExecution(req),
530
588
  });
531
589
  if ("error" in result)
532
590
  return json(res, 422, { ok: false, error: "binding apply rejected" }, true);
@@ -572,6 +630,53 @@ export function handleSettingsRequest(req, res, url, ctx) {
572
630
  .catch(err => json(res, 409, { error: err.message }));
573
631
  return true;
574
632
  }
633
+ // One hot web toggle; an unrelated Settings edit never materializes defaults.
634
+ if (method === "PUT" && path === "/api/settings/fleet/web") {
635
+ if (!cfg) {
636
+ json(res, 503, { error: "fleet not loaded" });
637
+ return true;
638
+ }
639
+ readBody(req, 4096).then(buf => {
640
+ if (!permitWebContinuation(req, res, ctx))
641
+ return;
642
+ let body;
643
+ try {
644
+ body = JSON.parse(buf.toString("utf-8"));
645
+ }
646
+ catch {
647
+ return json(res, 400, { error: "invalid JSON" });
648
+ }
649
+ if (!body || typeof body !== "object" || Array.isArray(body)
650
+ || Object.keys(body).some(key => key !== "echo_to_channel" && key !== "public_link"))
651
+ return json(res, 400, { error: "unknown web field" });
652
+ const patch = body;
653
+ if (patch.echo_to_channel !== undefined && typeof patch.echo_to_channel !== "boolean")
654
+ return json(res, 400, { error: "echo_to_channel must be a boolean" });
655
+ if (patch.public_link !== undefined && !validPublicLinkPatch(patch.public_link))
656
+ return json(res, 400, { error: "invalid public_link settings" });
657
+ const changes = [];
658
+ const web = { ...cfg.web };
659
+ if (typeof patch.echo_to_channel === "boolean") {
660
+ web.echo_to_channel = patch.echo_to_channel;
661
+ changes.push({ path: ["web", "echo_to_channel"], value: patch.echo_to_channel });
662
+ }
663
+ if (validPublicLinkPatch(patch.public_link)) {
664
+ if (Object.keys(patch.public_link).length)
665
+ web.public_link = { ...web.public_link, ...patch.public_link };
666
+ for (const [key, value] of Object.entries(patch.public_link))
667
+ changes.push({ path: ["web", "public_link", key], value });
668
+ }
669
+ if (!changes.length)
670
+ return json(res, 200, { ok: true });
671
+ const before = validateFleetConfig(cfg);
672
+ const after = validateFleetConfig({ ...cfg, web });
673
+ if (rejectIfWorse(res, before, after))
674
+ return;
675
+ settingsWrite(req, () => { cfg.web = web; ctx.saveFleetConfig(changes); });
676
+ json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
677
+ }).catch(() => json(res, 400, { error: "bad request" }));
678
+ return true;
679
+ }
575
680
  // ── Fleet defaults ──
576
681
  if (method === "PUT" && path === "/api/settings/fleet/defaults") {
577
682
  if (!cfg) {
@@ -579,6 +684,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
579
684
  return true;
580
685
  }
581
686
  readBody(req, 512 * 1024).then(buf => {
687
+ if (!permitWebContinuation(req, res, ctx))
688
+ return;
582
689
  let body;
583
690
  try {
584
691
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -597,8 +704,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
597
704
  const after = validateFleetConfig({ ...cfg, defaults: merged });
598
705
  if (rejectIfWorse(res, before, after))
599
706
  return;
600
- cfg.defaults = merged;
601
- ctx.saveFleetConfig();
707
+ settingsWrite(req, () => { cfg.defaults = merged; ctx.saveFleetConfig(); });
602
708
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
603
709
  }).catch(() => json(res, 400, { error: "bad request" }));
604
710
  return true;
@@ -610,6 +716,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
610
716
  return true;
611
717
  }
612
718
  readBody(req, 512 * 1024).then(buf => {
719
+ if (!permitWebContinuation(req, res, ctx))
720
+ return;
613
721
  let body;
614
722
  try {
615
723
  body = JSON.parse(buf.toString("utf-8") || "[]");
@@ -650,6 +758,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
650
758
  const id = typeof candidate.id === "string" ? candidate.id
651
759
  : typeof candidate.type === "string" ? candidate.type : `channel-${index}`;
652
760
  const previous = currentById.get(id);
761
+ const unowned = unownedChannelField(candidate, previous);
762
+ if (unowned)
763
+ return json(res, 400, { ok: false, error: `unsupported connection field: channels[${index}].${unowned}` }, true);
653
764
  const tokenEnv = typeof candidate.bot_token_env === "string" ? candidate.bot_token_env : null;
654
765
  if (tokenEnv && (providerRegistryEnvKeys().has(tokenEnv) || isReservedProviderEnvKey(tokenEnv))) {
655
766
  return json(res, 409, { ok: false, error: "bot token env conflicts with a protected provider secret key" }, true);
@@ -680,9 +791,22 @@ export function handleSettingsRequest(req, res, url, ctx) {
680
791
  const after = validateFleetConfig(next);
681
792
  if (rejectIfWorse(res, before, after))
682
793
  return;
683
- cfg.channels = normalizedBody;
684
- delete cfg.channel;
685
- ctx.saveFleetConfig();
794
+ const previous = { channels: cfg.channels, channel: cfg.channel, hadChannel: "channel" in cfg };
795
+ try {
796
+ settingsWrite(req, () => {
797
+ cfg.channels = normalizedBody;
798
+ delete cfg.channel;
799
+ ctx.saveFleetConfig();
800
+ });
801
+ }
802
+ catch (err) {
803
+ // #1056: the save refused (fleet.yaml unchanged): memory goes back to what the file still says.
804
+ cfg.channels = previous.channels;
805
+ if (previous.hadChannel)
806
+ cfg.channel = previous.channel;
807
+ ctx.logger.warn({ err }, "settings: channels save refused");
808
+ return json(res, 500, { ok: false, error: err.message });
809
+ }
686
810
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
687
811
  }).catch(() => json(res, 400, { error: "bad request" }));
688
812
  return true;
@@ -690,6 +814,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
690
814
  // ── Classic defaults ──
691
815
  if (method === "PUT" && path === "/api/settings/classic/defaults") {
692
816
  readBody(req, 512 * 1024).then(buf => {
817
+ if (!permitWebContinuation(req, res, ctx))
818
+ return;
693
819
  let body;
694
820
  try {
695
821
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -717,7 +843,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
717
843
  return;
718
844
  classic.defaults = merged;
719
845
  try {
720
- writeClassicAtomic(ctx, classic);
846
+ settingsWrite(req, () => writeClassicAtomic(ctx, classic));
721
847
  }
722
848
  catch (err) {
723
849
  ctx.logger.warn({ err }, "settings: failed to atomically update classicBot.yaml");
@@ -744,6 +870,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
744
870
  return true;
745
871
  }
746
872
  readBody(req, 512 * 1024).then(async (buf) => {
873
+ if (!permitWebContinuation(req, res, ctx))
874
+ return;
747
875
  let body;
748
876
  try {
749
877
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -761,6 +889,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
761
889
  "context_lines",
762
890
  "tool_progress",
763
891
  "reply_completion_guard",
892
+ "web_echo",
764
893
  ]);
765
894
  const unknown = Object.keys(body).filter(field => !allowed.has(field));
766
895
  if (unknown.length)
@@ -787,6 +916,9 @@ export function handleSettingsRequest(req, res, url, ctx) {
787
916
  && typeof body.reply_completion_guard !== "boolean") {
788
917
  return json(res, 400, { error: "reply_completion_guard must be a boolean" });
789
918
  }
919
+ if (body.web_echo !== undefined && typeof body.web_echo !== "boolean") {
920
+ return json(res, 400, { error: "web_echo must be a boolean" });
921
+ }
790
922
  let classic;
791
923
  try {
792
924
  classic = readClassic(ctx);
@@ -797,7 +929,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
797
929
  const channels = classic.channels;
798
930
  if (!channels || typeof channels !== "object" || Array.isArray(channels))
799
931
  return json(res, 404, { error: "classic channel not found" });
800
- const current = channels[key];
932
+ const current = Object.hasOwn(channels, key) ? channels[key] : undefined;
801
933
  if (!current || typeof current !== "object" || Array.isArray(current))
802
934
  return json(res, 404, { error: "classic channel not found" });
803
935
  const previous = structuredClone(classic);
@@ -816,29 +948,42 @@ export function handleSettingsRequest(req, res, url, ctx) {
816
948
  if (rejectIfWorse(res, before, after))
817
949
  return;
818
950
  try {
819
- writeClassicAtomic(ctx, classic);
951
+ settingsWrite(req, () => writeClassicAtomic(ctx, classic), false);
820
952
  }
821
953
  catch (err) {
822
954
  ctx.logger.warn({ err, key }, "settings: failed to atomically update classic channel");
823
955
  return json(res, 500, { error: "failed to write classicBot.yaml" });
824
956
  }
957
+ const undo = settingsUndo(classicPath(ctx), previous, classic, Object.keys(body).map(field => ["channels", key, field]));
958
+ const execution = settingsRequestExecution(req);
825
959
  const instanceName = typeof merged.instanceName === "string" ? merged.instanceName : undefined;
960
+ const restore = instanceName ? ctx.captureClassicSettingsRestoration?.(instanceName, Object.keys(body), execution) : undefined;
826
961
  try {
827
962
  if (instanceName && ctx.restartClassicInstanceFromSettings) {
828
- await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
963
+ if (execution)
964
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body), execution);
965
+ else
966
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
967
+ execution?.assert();
829
968
  }
830
969
  }
831
970
  catch (err) {
832
971
  // Keep disk and runtime consistent if the requested restart fails.
833
972
  try {
834
- writeClassicAtomic(ctx, previous);
973
+ const retained = undoSettingsPaths(classicPath(ctx), readClassic(ctx), undo);
974
+ writeClassicAtomic(ctx, retained.value);
975
+ if (retained.conflicts)
976
+ throw new Error("concurrent Classic edit retained");
835
977
  }
836
978
  catch (rollbackErr) {
837
979
  ctx.logger.error({ err: rollbackErr, key }, "settings: failed to roll back classic channel update");
838
980
  }
839
981
  if (instanceName && ctx.restartClassicInstanceFromSettings) {
840
982
  try {
841
- await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
983
+ if (restore)
984
+ await restore();
985
+ else
986
+ await ctx.restartClassicInstanceFromSettings(instanceName, Object.keys(body));
842
987
  }
843
988
  catch (recoveryErr) {
844
989
  ctx.logger.error({ err: recoveryErr, key, instanceName }, "settings: failed to restore classic instance after rollback");
@@ -847,6 +992,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
847
992
  ctx.logger.warn({ err, key, instanceName }, "settings: classic channel restart failed; config rolled back");
848
993
  return json(res, 409, { error: `classic instance restart failed: ${err.message}` });
849
994
  }
995
+ execution?.complete();
850
996
  ctx.logger.info({ key, instanceName }, "settings: updated classic channel");
851
997
  const hotOnly = Object.keys(body).length > 0
852
998
  && Object.keys(body).every(field => CLASSIC_HOT_CONFIG_KEYS.has(field));
@@ -871,6 +1017,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
871
1017
  return true;
872
1018
  }
873
1019
  readBody(req, 64 * 1024).then(buf => {
1020
+ if (!permitWebContinuation(req, res, ctx))
1021
+ return;
874
1022
  let body = {};
875
1023
  try {
876
1024
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -919,6 +1067,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
919
1067
  return true;
920
1068
  }
921
1069
  readBody(req, 64 * 1024).then(async (buf) => {
1070
+ if (!permitWebContinuation(req, res, ctx))
1071
+ return;
922
1072
  let body = {};
923
1073
  try {
924
1074
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -983,9 +1133,6 @@ export function handleSettingsRequest(req, res, url, ctx) {
983
1133
  }
984
1134
  // ── Instances (create / patch / delete) ──
985
1135
  const validName = (n) => !!n && /^[^\\/\x00]+$/.test(n);
986
- const nullableInstanceOverrides = new Set(["model", "auto_pause_after", "hang_detector", "agent_mode", "tool_set", "tool_progress", "reply_completion_guard", "log_level", "lightweight", "model_failover", "display_name", "status_emojis", "cross_instance_visibility"]);
987
- const removesInstanceOverride = (key, value) => nullableInstanceOverrides.has(key)
988
- && (value === null || (key === "model" && typeof value === "string" && value.trim() === ""));
989
1136
  const rawInstancePatches = (name, patch) => {
990
1137
  const changes = [];
991
1138
  for (const [key, value] of Object.entries(patch)) {
@@ -1008,33 +1155,24 @@ export function handleSettingsRequest(req, res, url, ctx) {
1008
1155
  }
1009
1156
  const base = (exists ? cfg.instances[name] : {});
1010
1157
  const patch = body;
1011
- const mergedInst = { ...base, ...patch };
1012
- if (patch.hang_detector && typeof patch.hang_detector === "object" && !Array.isArray(patch.hang_detector)) {
1013
- const hangPatch = patch.hang_detector;
1014
- const mergedHang = { ...(base.hang_detector ?? {}), ...hangPatch };
1015
- // Nested null removes only the timeout override while preserving any
1016
- // independently configured `enabled` override.
1017
- if (hangPatch.timeout_minutes === null)
1018
- delete mergedHang.timeout_minutes;
1019
- if (Object.keys(mergedHang).length)
1020
- mergedInst.hang_detector = mergedHang;
1021
- else
1022
- delete mergedInst.hang_detector;
1158
+ let mergedInst;
1159
+ try {
1160
+ mergedInst = normalizeSettingsInstancePatch(base, patch);
1023
1161
  }
1024
- // JSON has no `undefined`; null is the PATCH sentinel for removing an
1025
- // optional override so the instance inherits the fleet default again.
1026
- for (const key of nullableInstanceOverrides) {
1027
- if (removesInstanceOverride(key, patch[key]))
1028
- delete mergedInst[key];
1162
+ catch {
1163
+ json(res, 400, { error: "unsupported_instance_null" });
1164
+ return;
1029
1165
  }
1030
1166
  const before = validateFleetConfig(cfg);
1031
1167
  const after = validateFleetConfig({ ...cfg, instances: { ...cfg.instances, [name]: mergedInst } });
1032
1168
  if (rejectIfWorse(res, before, after))
1033
1169
  return;
1034
- cfg.instances[name] = mergedInst;
1035
- if (!exists)
1036
- clearPausedMarker(join(ctx.dataDir, "instances", name));
1037
- ctx.saveFleetConfig(rawInstancePatches(name, patch));
1170
+ settingsWrite(req, () => {
1171
+ Object.defineProperty(cfg.instances, name, { value: mergedInst, writable: true, enumerable: true, configurable: true });
1172
+ if (!exists)
1173
+ clearPausedMarker(join(ctx.dataDir, "instances", name));
1174
+ ctx.saveFleetConfig(rawInstancePatches(name, patch));
1175
+ });
1038
1176
  json(res, 200, { ok: true, warnings: saveWarnings(before, after) });
1039
1177
  };
1040
1178
  // POST /api/settings/fleet/instances — create, name taken from the body.
@@ -1044,6 +1182,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
1044
1182
  return true;
1045
1183
  }
1046
1184
  readBody(req, 512 * 1024).then(buf => {
1185
+ if (!permitWebContinuation(req, res, ctx))
1186
+ return;
1047
1187
  let body;
1048
1188
  try {
1049
1189
  body = JSON.parse(buf.toString("utf-8") || "{}");
@@ -1056,7 +1196,7 @@ export function handleSettingsRequest(req, res, url, ctx) {
1056
1196
  const name = typeof body.name === "string" ? body.name.trim() : "";
1057
1197
  if (!validName(name))
1058
1198
  return json(res, 400, { error: "missing or invalid instance name (provide `name` in the body)" });
1059
- if (cfg.instances[name])
1199
+ if (Object.hasOwn(cfg.instances, name))
1060
1200
  return json(res, 409, { error: "instance already exists" });
1061
1201
  const { name: _n, ...instBody } = body;
1062
1202
  commitInstance(name, false, instBody);
@@ -1076,19 +1216,21 @@ export function handleSettingsRequest(req, res, url, ctx) {
1076
1216
  return true;
1077
1217
  }
1078
1218
  if (method === "DELETE") {
1079
- if (!cfg.instances[name]) {
1219
+ if (!Object.hasOwn(cfg.instances, name)) {
1080
1220
  json(res, 404, { error: "instance not found" });
1081
1221
  return true;
1082
1222
  }
1083
1223
  // DELETE never blocks on validation; surface any resulting warnings.
1084
- delete cfg.instances[name];
1085
- clearPausedMarker(join(ctx.dataDir, "instances", name));
1086
- ctx.saveFleetConfig();
1224
+ settingsWrite(req, () => {
1225
+ delete cfg.instances[name];
1226
+ clearPausedMarker(join(ctx.dataDir, "instances", name));
1227
+ ctx.saveFleetConfig();
1228
+ });
1087
1229
  json(res, 200, { ok: true, warnings: validateFleetConfig(cfg).warnings });
1088
1230
  return true;
1089
1231
  }
1090
1232
  if (method === "POST" || method === "PATCH") {
1091
- const exists = !!cfg.instances[name];
1233
+ const exists = Object.hasOwn(cfg.instances, name);
1092
1234
  if (method === "POST" && exists) {
1093
1235
  json(res, 409, { error: "instance already exists" });
1094
1236
  return true;
@@ -1098,6 +1240,8 @@ export function handleSettingsRequest(req, res, url, ctx) {
1098
1240
  return true;
1099
1241
  }
1100
1242
  readBody(req, 512 * 1024).then(buf => {
1243
+ if (!permitWebContinuation(req, res, ctx))
1244
+ return;
1101
1245
  let body;
1102
1246
  try {
1103
1247
  body = JSON.parse(buf.toString("utf-8") || "{}");