toolroll 0.9.62 → 0.9.65

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 (345) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +66 -8
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +203 -10
  16. package/dist/agent-fence.js +623 -41
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +263 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +199 -17
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/check-levels.js +3 -0
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +20 -1
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +5 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +24 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/cli.d.ts +8 -0
  71. package/dist/contracts/cli.js +3 -1
  72. package/dist/contracts/console-api.d.ts +30 -4
  73. package/dist/contracts/console-api.js +13 -4
  74. package/dist/contracts/flow-card.d.ts +4 -0
  75. package/dist/contracts/flow-card.js +3 -0
  76. package/dist/contracts/flow.d.ts +46 -0
  77. package/dist/contracts/flow.js +11 -2
  78. package/dist/contracts/integration-metadata.d.ts +2 -1
  79. package/dist/contracts/integration-metadata.js +2 -1
  80. package/dist/contracts/lead-tools.d.ts +92 -0
  81. package/dist/contracts/lead-tools.js +13 -0
  82. package/dist/contracts/provider.d.ts +11 -1
  83. package/dist/contracts/provider.js +1 -1
  84. package/dist/contracts/route.d.ts +76 -1
  85. package/dist/contracts/route.js +1 -1
  86. package/dist/contracts/store-json.d.ts +2 -0
  87. package/dist/control-setup.d.ts +1 -1
  88. package/dist/control-ui.js +2 -2
  89. package/dist/converse.d.ts +16 -0
  90. package/dist/converse.js +32 -4
  91. package/dist/demo-chat.js +4 -4
  92. package/dist/demo.js +2 -2
  93. package/dist/desktop-update-gate.js +6 -0
  94. package/dist/discord-chat.js +4 -1
  95. package/dist/dispatch.js +7 -4
  96. package/dist/doctor.d.ts +53 -0
  97. package/dist/doctor.js +427 -0
  98. package/dist/egress-bridge.d.ts +17 -0
  99. package/dist/egress-bridge.js +111 -0
  100. package/dist/egress.d.ts +119 -0
  101. package/dist/egress.js +423 -0
  102. package/dist/evidence-pack.d.ts +1 -1
  103. package/dist/evidence-pack.js +2 -2
  104. package/dist/exec.d.ts +81 -0
  105. package/dist/exec.js +368 -9
  106. package/dist/export-ui.js +2 -2
  107. package/dist/flow-code.d.ts +2 -2
  108. package/dist/flow-code.js +4 -15
  109. package/dist/flow-engine.js +35 -11
  110. package/dist/flow-gallery-ui.js +2 -2
  111. package/dist/flow-gallery.js +27 -8
  112. package/dist/flow-share.js +16 -5
  113. package/dist/flow-triggers.d.ts +84 -11
  114. package/dist/flow-triggers.js +554 -87
  115. package/dist/flow-unattended-words.d.ts +28 -0
  116. package/dist/flow-unattended-words.js +50 -0
  117. package/dist/flow-unattended.d.ts +227 -0
  118. package/dist/flow-unattended.js +776 -0
  119. package/dist/flows-cli.d.ts +28 -1
  120. package/dist/flows-cli.js +63 -12
  121. package/dist/flows-ui.d.ts +2 -3
  122. package/dist/flows-ui.js +56 -5
  123. package/dist/flows.d.ts +4 -0
  124. package/dist/flows.js +9 -1
  125. package/dist/google-mail.d.ts +1 -0
  126. package/dist/google-mail.js +61 -18
  127. package/dist/html.d.ts +9 -0
  128. package/dist/html.js +11 -0
  129. package/dist/integrations-ui.d.ts +2 -1
  130. package/dist/integrations-ui.js +6 -3
  131. package/dist/integrations.d.ts +17 -3
  132. package/dist/integrations.js +38 -18
  133. package/dist/invoke.d.ts +17 -0
  134. package/dist/invoke.js +510 -182
  135. package/dist/keys.d.ts +18 -14
  136. package/dist/keys.js +5 -0
  137. package/dist/landed-cleanup.d.ts +3 -2
  138. package/dist/landed-cleanup.js +12 -5
  139. package/dist/lead-capabilities.js +3 -2
  140. package/dist/lead-contract.js +2 -0
  141. package/dist/lead-doors.d.ts +2 -0
  142. package/dist/lead-doors.js +7 -0
  143. package/dist/lead-follow.d.ts +2 -0
  144. package/dist/lead-follow.js +58 -2
  145. package/dist/lead-progress.js +1 -0
  146. package/dist/lead-queue-runtime.d.ts +46 -0
  147. package/dist/lead-queue-runtime.js +191 -0
  148. package/dist/lead-queue.d.ts +104 -0
  149. package/dist/lead-queue.js +187 -0
  150. package/dist/lead-sweep.d.ts +67 -0
  151. package/dist/lead-sweep.js +99 -0
  152. package/dist/lead-tools.d.ts +22 -0
  153. package/dist/lead-tools.js +163 -4
  154. package/dist/lead.d.ts +13 -5
  155. package/dist/lead.js +89 -29
  156. package/dist/limits-ui.d.ts +8 -1
  157. package/dist/limits-ui.js +26 -0
  158. package/dist/live-bus.d.ts +8 -0
  159. package/dist/live-bus.js +40 -4
  160. package/dist/live-detail.d.ts +27 -0
  161. package/dist/live-detail.js +57 -0
  162. package/dist/live-notes.d.ts +20 -0
  163. package/dist/live-notes.js +47 -0
  164. package/dist/live-records.d.ts +140 -0
  165. package/dist/live-records.js +935 -0
  166. package/dist/live-terminal.d.ts +258 -0
  167. package/dist/live-terminal.js +649 -0
  168. package/dist/live.d.ts +38 -27
  169. package/dist/live.js +142 -124
  170. package/dist/mcp-connect.d.ts +8 -7
  171. package/dist/mcp-connect.js +49 -51
  172. package/dist/mcp-oauth.d.ts +6 -3
  173. package/dist/mcp-oauth.js +8 -4
  174. package/dist/memory-pass.js +2 -2
  175. package/dist/mobile-viewport.d.ts +5 -1
  176. package/dist/mobile-viewport.js +18 -2
  177. package/dist/model-catalog.js +4 -0
  178. package/dist/monitoring-ui.js +2 -2
  179. package/dist/needs-you.d.ts +1 -1
  180. package/dist/needs-you.js +11 -4
  181. package/dist/oauth-run-token.d.ts +51 -0
  182. package/dist/oauth-run-token.js +127 -0
  183. package/dist/operate.d.ts +2 -0
  184. package/dist/operate.js +282 -18
  185. package/dist/passwordless.d.ts +23 -0
  186. package/dist/passwordless.js +26 -0
  187. package/dist/peek-cli.d.ts +12 -4
  188. package/dist/peek-cli.js +19 -11
  189. package/dist/phase-routing.js +10 -3
  190. package/dist/plan-auto.d.ts +11 -0
  191. package/dist/plan-auto.js +62 -11
  192. package/dist/planner-source.d.ts +3 -0
  193. package/dist/planner-source.js +14 -0
  194. package/dist/planner.d.ts +2 -1
  195. package/dist/planner.js +7 -1
  196. package/dist/policy-ui.js +2 -2
  197. package/dist/policy.d.ts +1 -20
  198. package/dist/policy.js +29 -2
  199. package/dist/process-recovery-eligibility.d.ts +1 -1
  200. package/dist/project-access.d.ts +3 -0
  201. package/dist/project-access.js +3 -0
  202. package/dist/project-delete-ui.d.ts +12 -0
  203. package/dist/project-delete-ui.js +41 -9
  204. package/dist/project-delete.d.ts +3 -1
  205. package/dist/project-delete.js +12 -6
  206. package/dist/project-file.d.ts +3 -0
  207. package/dist/project-file.js +16 -0
  208. package/dist/project-memory.d.ts +3 -1
  209. package/dist/project-memory.js +57 -25
  210. package/dist/project-tools.d.ts +103 -27
  211. package/dist/project-tools.js +399 -100
  212. package/dist/provider-auth.d.ts +4 -3
  213. package/dist/provider-auth.js +5 -4
  214. package/dist/provider-connection.d.ts +2 -1
  215. package/dist/provider-connection.js +5 -0
  216. package/dist/provider-home.d.ts +22 -0
  217. package/dist/provider-home.js +99 -0
  218. package/dist/provider.d.ts +42 -5
  219. package/dist/provider.js +97 -19
  220. package/dist/pull-request-ui.js +2 -2
  221. package/dist/push.js +1 -1
  222. package/dist/quick-start.d.ts +24 -0
  223. package/dist/quick-start.js +127 -0
  224. package/dist/release.d.ts +3 -0
  225. package/dist/release.js +14 -8
  226. package/dist/remote-main.d.ts +4 -0
  227. package/dist/remote-main.js +3 -3
  228. package/dist/request-budget-ui.js +2 -2
  229. package/dist/result-review.d.ts +1 -1
  230. package/dist/result-review.js +9 -3
  231. package/dist/retention-ui.js +2 -2
  232. package/dist/retention.js +10 -1
  233. package/dist/run-efficiency.d.ts +73 -0
  234. package/dist/run-efficiency.js +228 -0
  235. package/dist/runner.d.ts +5 -1
  236. package/dist/runner.js +22 -1
  237. package/dist/scope.d.ts +171 -6
  238. package/dist/scope.js +251 -31
  239. package/dist/scout-net.d.ts +4 -1
  240. package/dist/scout-net.js +37 -12
  241. package/dist/scout.d.ts +20 -6
  242. package/dist/scout.js +27 -3
  243. package/dist/serve.js +258 -43
  244. package/dist/server/chat.d.ts +1 -1
  245. package/dist/server/chat.js +122 -31
  246. package/dist/server/chrome.d.ts +6 -1
  247. package/dist/server/chrome.js +18 -7
  248. package/dist/server/console.css +52 -38
  249. package/dist/server/flows.js +65 -13
  250. package/dist/server/guards.d.ts +1 -0
  251. package/dist/server/guards.js +23 -9
  252. package/dist/server/http.d.ts +7 -0
  253. package/dist/server/http.js +3 -2
  254. package/dist/server/live.d.ts +34 -1
  255. package/dist/server/live.js +39 -17
  256. package/dist/server/pages-assets.js +5 -0
  257. package/dist/server/pages.js +90 -13
  258. package/dist/server/people-tokens.d.ts +3 -0
  259. package/dist/server/people-tokens.js +55 -17
  260. package/dist/server/remote-hooks.js +12 -6
  261. package/dist/server/remote.d.ts +2 -1
  262. package/dist/server/remote.js +14 -5
  263. package/dist/server/render-chat.d.ts +12 -3
  264. package/dist/server/render-chat.js +47 -16
  265. package/dist/server/render-pages.d.ts +4 -1
  266. package/dist/server/render-pages.js +5 -4
  267. package/dist/server/render-people.d.ts +3 -2
  268. package/dist/server/render-people.js +3 -2
  269. package/dist/server/render-results.d.ts +18 -2
  270. package/dist/server/render-results.js +37 -28
  271. package/dist/server/render-settings.js +6 -5
  272. package/dist/server/render-tasks.d.ts +35 -10
  273. package/dist/server/render-tasks.js +110 -27
  274. package/dist/server/request-context.d.ts +2 -0
  275. package/dist/server/request-context.js +2 -1
  276. package/dist/server/route-table.d.ts +3 -0
  277. package/dist/server/route-table.js +49 -5
  278. package/dist/server/runtime.d.ts +34 -3
  279. package/dist/server/session.d.ts +25 -1
  280. package/dist/server/session.js +18 -4
  281. package/dist/server/settings.js +132 -13
  282. package/dist/server/sweeps.d.ts +4 -0
  283. package/dist/server/sweeps.js +58 -0
  284. package/dist/server/task-views.d.ts +2 -1
  285. package/dist/server/task-views.js +96 -6
  286. package/dist/server/tasks.js +320 -74
  287. package/dist/server/terminals.d.ts +31 -0
  288. package/dist/server/terminals.js +365 -0
  289. package/dist/setup-guide.js +3 -2
  290. package/dist/slack-chat.js +4 -1
  291. package/dist/spend-ui.js +3 -3
  292. package/dist/spend.js +2 -1
  293. package/dist/steer-capabilities.d.ts +32 -0
  294. package/dist/steer-capabilities.js +60 -0
  295. package/dist/steer-controls.d.ts +14 -0
  296. package/dist/steer-controls.js +36 -0
  297. package/dist/steer-status.js +2 -2
  298. package/dist/storage-sweep-child.js +3 -1
  299. package/dist/storage-sweep-process.d.ts +12 -5
  300. package/dist/storage-sweep-process.js +23 -6
  301. package/dist/storage-ui.js +4 -4
  302. package/dist/store-baseline.d.ts +1 -1
  303. package/dist/store-baseline.js +1 -1
  304. package/dist/store.d.ts +466 -19
  305. package/dist/store.js +1185 -63
  306. package/dist/subagent-desk.js +1 -1
  307. package/dist/subagent-tools.d.ts +1 -1
  308. package/dist/summary.js +3 -0
  309. package/dist/surface.js +6 -4
  310. package/dist/task-source.d.ts +58 -0
  311. package/dist/task-source.js +93 -0
  312. package/dist/team-leads.js +1 -1
  313. package/dist/teams-chat.js +4 -1
  314. package/dist/teardown-git.d.ts +14 -0
  315. package/dist/teardown-git.js +119 -0
  316. package/dist/telegram-lead.d.ts +3 -0
  317. package/dist/telegram-lead.js +44 -7
  318. package/dist/telegram.d.ts +11 -2
  319. package/dist/telegram.js +51 -17
  320. package/dist/terminal-catalog.d.ts +11 -0
  321. package/dist/terminal-catalog.js +129 -0
  322. package/dist/text-limits.d.ts +2 -0
  323. package/dist/text-limits.js +2 -0
  324. package/dist/tool-broker.d.ts +39 -0
  325. package/dist/tool-broker.js +370 -0
  326. package/dist/tool-launcher.js +5 -4
  327. package/dist/tool-secret-store.d.ts +170 -0
  328. package/dist/tool-secret-store.js +834 -0
  329. package/dist/toolroll-update-ui.js +2 -2
  330. package/dist/toolroll-update.js +14 -3
  331. package/dist/tools-ui.d.ts +7 -1
  332. package/dist/tools-ui.js +19 -10
  333. package/dist/webhook-limits.d.ts +23 -0
  334. package/dist/webhook-limits.js +30 -0
  335. package/dist/webhook-readers.d.ts +56 -0
  336. package/dist/webhook-readers.js +138 -0
  337. package/dist/webhook-signing.d.ts +27 -0
  338. package/dist/webhook-signing.js +119 -0
  339. package/dist/work-index.d.ts +29 -0
  340. package/dist/work-index.js +21 -5
  341. package/dist/workspace-revision.js +3 -1
  342. package/dist/worktree.d.ts +10 -4
  343. package/dist/worktree.js +70 -29
  344. package/package.json +13 -2
  345. package/scripts/provider-canary.mjs +210 -31
@@ -18,9 +18,10 @@ import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people
18
18
  import { saveScript } from "../flow-scripts.js";
19
19
  import { chooseFlowCard } from "../flow-send.js";
20
20
  import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
21
- import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
21
+ import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries } from "../flow-triggers.js";
22
22
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
23
23
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
24
+ import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
24
25
  import { keyStatus } from "../keys.js";
25
26
  import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
26
27
  import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
@@ -42,7 +43,7 @@ import { redirect, respond, SAFETY, taskHref } from "./http.js";
42
43
  import { html } from "../html.js";
43
44
  import {} from "./session.js";
44
45
  export function createFlowsHandlers(runtime) {
45
- const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
46
+ const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
46
47
  // ---- reads ---------------------------------------------------------------
47
48
  /** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
48
49
  function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
@@ -148,7 +149,7 @@ export function createFlowsHandlers(runtime) {
148
149
  return refuse(response, who, 404, "No such flow in your projects.", "/flows");
149
150
  const selected = Number(url.searchParams.get("card")), start = Number(url.searchParams.get("start"));
150
151
  const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos: consoleProjects(), startTrigger: Number.isSafeInteger(start) && start > 0 ? start : null,
151
- sortReady: keyStatus("openrouter", providerHome).set, toolHome });
152
+ sortReady: keyStatus("openrouter", providerHome).set, toolHome, now: clock() });
152
153
  if (url.searchParams.get("format") === "json")
153
154
  return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
154
155
  return sendScreen(response, 200, screen(flow.name, html `<p><a href="/flows">Flows</a></p><h1>${flow.name}</h1>${flowFallbackHtml(view)}`, { chrome: chromeFor(flow.repo, "flows"), workspace: { view } }));
@@ -546,7 +547,7 @@ export function createFlowsHandlers(runtime) {
546
547
  }
547
548
  const definition = flowDefinitionOf(flow);
548
549
  const dir = options.configDir ?? null;
549
- const viewNow = () => flowView(store, store.getFlow(flow.id) ?? flow, { name: who.name, approver: true }, null, { dir, repos: projects, sortReady: keyStatus("openrouter", providerHome).set, toolHome });
550
+ const viewNow = () => flowView(store, store.getFlow(flow.id) ?? flow, { name: who.name, approver: true }, null, { dir, repos: projects, sortReady: keyStatus("openrouter", providerHome).set, toolHome, now });
550
551
  const settle = (said, extra = {}) => {
551
552
  // Move what can move right away (a message, a decision's next zone); workers file and run the tasks.
552
553
  try {
@@ -571,7 +572,13 @@ export function createFlowsHandlers(runtime) {
571
572
  catch {
572
573
  return answer(400, { ok: false, said: "That trigger couldn't be read." });
573
574
  }
574
- const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir);
575
+ // A trigger that takes deliveries mints an address: a credential act (docs/plans/passwordless.md).
576
+ const ingress = rawTakesDeliveries(raw);
577
+ if (ingress && !authenticateCredential(who, body.get("password") ?? "").ok)
578
+ return answer(403, { ok: false, said: "Enter your Toolroll password to make a webhook address." });
579
+ const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir, ingress ? { ingress: "proved" } : {});
580
+ if (made.ok)
581
+ await prepareUnattendedCandidates(store, flow.id);
575
582
  return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
576
583
  }
577
584
  if (action === "scripts") {
@@ -579,8 +586,10 @@ export function createFlowsHandlers(runtime) {
579
586
  if (body.get("approve") === "yes")
580
587
  return store.approveFlowScript(flow.repo, body.get("name") ?? "") ? settle("Approved. Zones that run it go on.") : answer(404, { ok: false, said: "That script isn't waiting for approval." });
581
588
  if (body.get("remove") === "yes")
582
- return store.removeFlowScript(flow.repo, body.get("name") ?? "") ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
589
+ return store.removeFlowScript(flow.repo, body.get("name") ?? "", who.name, now) ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
583
590
  const saved = saveScript(store, flow.repo, { name: body.get("name"), about: body.get("about"), body: body.get("body"), timeoutMinutes: body.get("timeoutMinutes"), language: body.get("language"), file: body.get("file") }, who.name, now);
591
+ if (saved.ok)
592
+ await prepareUnattendedCandidates(store, flow.id);
584
593
  return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
585
594
  }
586
595
  if (action === "linear-key" || action === "hooks-address") {
@@ -604,6 +613,19 @@ export function createFlowsHandlers(runtime) {
604
613
  store.archiveFlow(flow.id, who.name, now);
605
614
  return redirect(response, "/flows");
606
615
  }
616
+ if (action === "unattended") {
617
+ // The owner's password signs exactly the terms the panel showed; a flow saved meanwhile is refused (sawRevision).
618
+ if (!authenticateApprover(who, body.get("password") ?? "").ok)
619
+ return answer(403, { ok: false, said: "That password isn't right. Nothing was signed." });
620
+ const asked = parseUnattendedAsk({ days: body.get("days"), budget: body.get("budget"), runs: body.get("runs"), include: body.get("include") });
621
+ if (!asked.ok)
622
+ return answer(400, { ok: false, said: asked.message });
623
+ const signed = await signUnattended(store, { flow: flow.id, signer: who.name, ask: asked.ask, sawRevision: Number(body.get("revision")), sawSchedules: body.get("schedules") ?? "" }, now);
624
+ return signed.ok ? settle(`Runs on its own until ${untilWords(signed.grant.absoluteExpiry)}.`) : answer(409, { ok: false, said: signed.message });
625
+ }
626
+ if (action === "unattended-off") {
627
+ return revokeUnattended(store, flow.id, who.name, now) ? settle("It no longer runs on its own. New cards wait for you.") : answer(409, { ok: false, said: "It wasn't running on its own." });
628
+ }
607
629
  if (action === "save") {
608
630
  let saved, drawn;
609
631
  try {
@@ -637,6 +659,8 @@ export function createFlowsHandlers(runtime) {
637
659
  if (dir === null)
638
660
  return answer(409, { ok: false, said: "Secrets are kept on the console's computer." });
639
661
  const saved = setFlowSecret(dir, flow.repo, (body.get("name") ?? "").trim(), body.get("value") ?? "");
662
+ if (saved.ok)
663
+ await prepareUnattendedCandidates(store, flow.id);
640
664
  return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
641
665
  }
642
666
  // Adding a card.
@@ -702,13 +726,17 @@ export function createFlowsHandlers(runtime) {
702
726
  if (trigger === null || trigger.flow !== flow.id || trigger.state === "removed")
703
727
  return answer(404, { ok: false, said: "That trigger isn't on this flow." });
704
728
  if (verb === "pause" || verb === "resume") {
705
- setFlowTriggerOn(store, trigger, verb === "resume", now);
729
+ setFlowTriggerOn(store, trigger, verb === "resume", now, who.name);
706
730
  return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
707
731
  }
708
732
  if (verb === "remove") {
709
- removeFlowTrigger(store, trigger, now, dir);
733
+ removeFlowTrigger(store, trigger, now, dir, who.name);
710
734
  return settle("Trigger removed.");
711
735
  }
736
+ // A public form link and a new webhook address each open a way in: credential acts, a password typed now.
737
+ if ((verb === "share" || verb === "renew") && !authenticateCredential(who, body.get("password") ?? "").ok) {
738
+ return answer(403, { ok: false, said: verb === "share" ? "Enter your Toolroll password to share a form link." : "Enter your Toolroll password to make a new address." });
739
+ }
712
740
  if (verb === "share") {
713
741
  const shared = shareFlowButton(store, trigger, now, dir);
714
742
  return shared.ok ? settle(shared.said, { reveal: shared.reveal }) : answer(409, { ok: false, said: shared.message });
@@ -717,6 +745,11 @@ export function createFlowsHandlers(runtime) {
717
745
  stopSharingFlowButton(store, trigger, now);
718
746
  return settle("The form link no longer works.");
719
747
  }
748
+ // v126: a person has seen that deliveries were turned away (the banner stays until then).
749
+ if (verb === "dropped") {
750
+ store.clearFlowDeliveriesDropped(trigger.id);
751
+ return settle("Cleared.");
752
+ }
720
753
  if (verb === "press") {
721
754
  let answers;
722
755
  try {
@@ -744,11 +777,24 @@ export function createFlowsHandlers(runtime) {
744
777
  const renewed = renewFlowHook(store, trigger, now, dir);
745
778
  return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
746
779
  }
747
- // Linear's signing secret, pasted on this secure panel behind the password — never in chat.
748
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
749
- return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
750
- const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
751
- return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
780
+ // A webhook's credentials change only on this secure panel, behind the password — never in chat (v126): a secret
781
+ // pasted (Linear's, Sentry's, Vercel's), one made and shown once with a new address, or who signs its deliveries.
782
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
783
+ return answer(403, { ok: false, said: verb === "secret" ? "Enter your Toolroll password to save a secret." : "Enter your Toolroll password to change how this webhook is proved." });
784
+ if (verb === "setup") {
785
+ const made = setUpFlowHook(store, trigger, now, dir);
786
+ return made.ok ? settle(made.said, { reveal: made.reveal }) : answer(409, { ok: false, said: made.message });
787
+ }
788
+ if (verb === "scope") {
789
+ const scoped = setFlowHookScope(store, trigger, { project: body.get("project"), team: body.get("team") }, who.name, now);
790
+ return scoped.ok ? settle(scoped.said) : answer(400, { ok: false, said: scoped.message });
791
+ }
792
+ if (verb === "sender") {
793
+ const changed = changeFlowHookSender(store, trigger, body.get("sender"), who.name, now, dir);
794
+ return changed.ok ? settle(changed.said) : answer(400, { ok: false, said: changed.message });
795
+ }
796
+ const saved = saveSigningSecret(store, trigger, body.get("secret") ?? "", now, dir);
797
+ return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
752
798
  };
753
799
  const recipeAct = (step) => async (ctx) => {
754
800
  const { who, request, response, now, posted } = ctx;
@@ -887,6 +933,8 @@ export function createFlowsHandlers(runtime) {
887
933
  "flow.linear-key": flowAct("linear-key"),
888
934
  "flow.hooks-address": flowAct("hooks-address"),
889
935
  "flow.secrets": flowAct("secrets"),
936
+ "flow.unattended": flowAct("unattended"),
937
+ "flow.unattended-off": flowAct("unattended-off"),
890
938
  "flow.triggers": flowAct("triggers"),
891
939
  "flow.card.move": cardAct("move"),
892
940
  "flow.card.decide": cardAct("decide"),
@@ -902,6 +950,10 @@ export function createFlowsHandlers(runtime) {
902
950
  "flow.trigger.press": triggerAct("press"),
903
951
  "flow.trigger.renew": triggerAct("renew"),
904
952
  "flow.trigger.secret": triggerAct("secret"),
953
+ "flow.trigger.setup": triggerAct("setup"),
954
+ "flow.trigger.sender": triggerAct("sender"),
955
+ "flow.trigger.scope": triggerAct("scope"),
956
+ "flow.trigger.dropped": triggerAct("dropped"),
905
957
  "flow.trigger.share": triggerAct("share"),
906
958
  "flow.trigger.unshare": triggerAct("unshare"),
907
959
  "recipes.prepare-send": recipeAct("prepare"),
@@ -41,6 +41,7 @@ export interface GuardsRuntime {
41
41
  }
42
42
  export declare function createSharedGuards(runtime: GuardsRuntime): {
43
43
  authenticateApprover: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
44
+ authenticateCredential: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
44
45
  authorizeMutation: (request: IncomingMessage, who: Who, body: FormView<FormFieldOf<"mutationGuard">>) => {
45
46
  status: number;
46
47
  message: string;
@@ -6,23 +6,29 @@ import { tokenLive, tokenProjects } from "../api-tokens.js";
6
6
  import { oauthProjects, oauthTokenAllowed } from "../mcp-oauth.js";
7
7
  import { REMOTE_MESSAGES, reproveRemote } from "../operate-remote.js";
8
8
  import { canonicalProject, rowVisible } from "../project.js";
9
- import { authenticateAccount, authenticateApprover as checkApproverPassword } from "../scope.js";
9
+ import { authenticateAccount, authenticateApprover as checkApproverPassword, credentialProof } from "../scope.js";
10
10
  import { SourceBudget } from "../sign-in-guard.js";
11
11
  import {} from "../store.js";
12
12
  import { requestContext } from "./request-context.js";
13
13
  import { refuse } from "./chrome.js";
14
14
  import { matchTaskPath } from "./http.js";
15
- import { PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_COOKIE, SESSION_IDLE_MS } from "./session.js";
15
+ import { PersistentSessions, SESSION_COOKIE, sessionWithinLifetime } from "./session.js";
16
16
  export function createSharedGuards(runtime) {
17
17
  // v99: wrong passwords. One address guessing across names runs out of tries here; each name
18
18
  // locks on its own (sign-in-guard.ts), and every lock, whatever road it came by, is kept.
19
19
  /**
20
- * Every step-up in the console (v100): a person the identity provider
21
- * checked in the last ten minutes confirms with that (an empty password
22
- * field); anyone else types their password. Standing (an approver, with
23
- * access to the project) is checked either way.
20
+ * Every step-up in the console: a password typed now, or (an empty field) an explicit identity-provider re-check
21
+ * made on request and not yet spent, or — inside an allowlisted act only — a remembered password (cockpit B2,
22
+ * scope.ts proveAccountNow / freshnessFor, reading the request's live facts). Standing (an approver, with access to
23
+ * the project) is checked either way.
24
24
  */
25
- // v100: the shared check honours a fresh identity-provider sign-in itself (freshIdentitySignIn, set per request below).
25
+ /** A credential act (docs/plans/passwordless.md): only a password typed now (or a one-use provider re-check) proves it. */
26
+ function authenticateCredential(who, token, repo) {
27
+ if (who.via !== "cookie")
28
+ return { ok: false, reason: "not-an-approver" };
29
+ const proved = checkApproverPassword(runtime.store, who.name, token, repo, { credential: true });
30
+ return !proved.ok || credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
31
+ }
26
32
  function authenticateApprover(who, token, repo) {
27
33
  // Re-entering a password never upgrades a machine credential into a browser ceremony.
28
34
  if (who.via !== "cookie")
@@ -332,7 +338,7 @@ export function createSharedGuards(runtime) {
332
338
  }
333
339
  for (const [id, session] of found === null ? [] : [found]) {
334
340
  const now = Date.now();
335
- if (now - session.lastSeen > SESSION_IDLE_MS || now - session.createdAt > SESSION_ABSOLUTE_MS) {
341
+ if (!sessionWithinLifetime(session, now)) {
336
342
  runtime.sessions.delete(id);
337
343
  return null;
338
344
  }
@@ -340,10 +346,18 @@ export function createSharedGuards(runtime) {
340
346
  runtime.sessions.delete(id);
341
347
  return null;
342
348
  }
349
+ // Ended in another process (or from Sessions): the cached copy is dropped, never used.
350
+ if (!runtime.sessions.stillSaved(id)) {
351
+ Map.prototype.delete.call(runtime.sessions, id);
352
+ return null;
353
+ }
343
354
  if (touch) {
344
355
  session.lastSeen = now;
345
356
  runtime.sessions.persist(session, true);
346
357
  }
358
+ // A touch that found the session ended elsewhere dropped it: this request is not signed in by it.
359
+ if (touch && !runtime.sessions.holds(session))
360
+ return null;
347
361
  return session;
348
362
  }
349
363
  return null;
@@ -356,5 +370,5 @@ export function createSharedGuards(runtime) {
356
370
  function runVisible(run) {
357
371
  return visible(taskRepoOf(run.taskRef));
358
372
  }
359
- return { authenticateApprover, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible };
373
+ return { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible };
360
374
  }
@@ -1,4 +1,5 @@
1
1
  /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
+ import type { TerminalHost } from "../live-terminal.js";
2
3
  import { type Html } from "../html.js";
3
4
  import { type CliHttpOptions, type RunOperateAs } from "../cli-http.js";
4
5
  import { type CodingWorkspace } from "../coding-workspace.js";
@@ -25,6 +26,8 @@ export type ServeOptions = {
25
26
  installBin?: string;
26
27
  /** Native coding workspace injection for isolated integration tests. */
27
28
  codingWorkspace?: CodingWorkspace;
29
+ /** Live terminals (tests stand their own in; null turns them off). Default: one on a machine that runs agents. */
30
+ terminalHost?: TerminalHost | null;
28
31
  /** Read-only worker connections for heavy reads (read-executor.ts); 0 reads in-process. Default: off under tests. */
29
32
  readWorkers?: number;
30
33
  store: Store;
@@ -141,6 +144,10 @@ export type ServeOptions = {
141
144
  connectFetch?: typeof fetch;
142
145
  /** Tests: every request sign-in with the identity provider makes (discovery, keys, tokens). */
143
146
  ssoFetch?: typeof fetch;
147
+ /** Cockpit B1: this `up` console's instance secret (null: none — the instance route answers 404). */
148
+ instanceSecret?: () => string | null;
149
+ /** Cockpit E: how Finder is asked to reveal a proved checkout (tests pass their own; never a launch). True: shown. */
150
+ revealPath?: (path: string) => Promise<boolean> | boolean;
144
151
  chatEnv?: Record<string, string | undefined>;
145
152
  /**
146
153
  * The live peek's locality ASSERTION (live-peek v3 §3): the administrator
@@ -1,4 +1,3 @@
1
- /** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
2
1
  import { html, htmlString, isHtml } from "../html.js";
3
2
  import { chatResultHref as sharedResultHref } from "../chat-controls.js";
4
3
  import {} from "../cli-http.js";
@@ -117,7 +116,9 @@ export function goOutside(response, to, words, bind) {
117
116
  response.end(htmlString(html `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${to}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${words} <a href="${to}">Continue</a></p>`));
118
117
  }
119
118
  export function redirect(response, to) {
120
- response.writeHead(303, { ...SAFETY, Location: to });
119
+ // Set as a header (not only in writeHead) so the answer can be read back after: a refused act's redirect names it.
120
+ response.setHeader("Location", to);
121
+ response.writeHead(303, { ...SAFETY });
121
122
  response.end();
122
123
  }
123
124
  export const taskChatHref = (taskId) => `/chat?task=${encodeURIComponent(taskId)}`;
@@ -1,10 +1,32 @@
1
+ /**
2
+ * GET /live: one stream per page, joined to every room the page shows.
3
+ *
4
+ * room=workspace the reader's workspace may have changed (any meaningful write, or a lapse)
5
+ * room=task:<id> the task family changed; who else has it open
6
+ * room=flow:<id>?card=&editing= the flow changed; who else is on it, and on which card
7
+ * room=chat[?task=|?project=] the reader's own lead reply, as it runs (the only room that carries content)
8
+ * room=team[?conversation=] the team conversation's cursor moved
9
+ * room=agent:<run> one agent on the Agents board changed; while open, this person watches it (no pings)
10
+ * room=summary the reader's project states (the sidebar): each change carries the snapshot; never watching or seeing
11
+ *
12
+ * Every room is validated before anything is looked up, and admitted with
13
+ * the same checks and answers as the page it belongs to, before the stream
14
+ * opens; each is re-proved on every signal and its safety-net check, and a
15
+ * viewer who no longer passes hears `gone`. Events name their room. A page
16
+ * that falls behind gets one `reload` and the stream ends, so its
17
+ * EventSource reconnects (live-bus.ts).
18
+ */
19
+ import type { IncomingMessage } from "node:http";
1
20
  import { type LiveBus } from "../live-bus.js";
2
21
  import type { WorkspaceRevision } from "../workspace-revision.js";
3
22
  import type { ServerRuntime } from "./runtime.js";
23
+ import type { Who } from "./session.js";
4
24
  /** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
5
- export declare const LIVE_ROOMS_PER_PAGE = 5;
25
+ export declare const LIVE_ROOMS_PER_PAGE = 6;
6
26
  export type LiveRoom = {
7
27
  kind: "workspace";
28
+ } | {
29
+ kind: "summary";
8
30
  } | {
9
31
  kind: "task";
10
32
  task: string;
@@ -26,10 +48,21 @@ export type LiveRoom = {
26
48
  };
27
49
  /** A room's name, read strictly: anything else is not a room. */
28
50
  export declare function parseLiveRoom(name: string): LiveRoom | null;
51
+ /** The same session, still signed in, from the cookie and the account (never this request's context): a live room's and
52
+ * a live terminal's re-proof. Null when it was signed out, ended elsewhere, revoked, rotated or re-roled. */
53
+ export declare function rereadBrowser(runtime: Pick<ServerRuntime, "identify" | "sessions" | "store">, request: IncomingMessage, who: Who & {
54
+ via: "cookie";
55
+ }): (Who & {
56
+ via: "cookie";
57
+ }) | null;
29
58
  export declare function createLiveHandlers(runtime: ServerRuntime, live: {
30
59
  bus: LiveBus;
31
60
  workspaceRevision: WorkspaceRevision;
32
61
  chatProjectOf: (projects: string[]) => string | null | undefined;
62
+ /** One account's sidebar snapshot for one scope (serve.ts projectSummaryFor); null: gone, or the scope moved. */
63
+ projectSummaryFor: (account: string, scope: string) => string | null;
64
+ /** What one account may see now (serve.ts projectScopeFor); null: no such account. */
65
+ projectScopeFor: (account: string) => string | null;
33
66
  }): {
34
67
  registrations: import("./handler-registry.js").Registration[];
35
68
  turnStarted: (thread: number) => void;
@@ -10,8 +10,8 @@ import { respond } from "./http.js";
10
10
  import { adapterPolicy } from "./route-policy.js";
11
11
  import { requestContext } from "./request-context.js";
12
12
  /** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
13
- export const LIVE_ROOMS_PER_PAGE = 5;
14
- const ROOM_NAME = /^(workspace|task|flow|chat|team|agent)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
13
+ export const LIVE_ROOMS_PER_PAGE = 6;
14
+ const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
15
15
  /** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
16
16
  const WORKSPACE_SETTLE_MS = 1_000;
17
17
  /** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
@@ -33,6 +33,7 @@ export function parseLiveRoom(name) {
33
33
  const only = (...allowed) => [...params.keys()].every(one => allowed.includes(one)) && allowed.every(one => params.getAll(one).length <= 1);
34
34
  switch (kind) {
35
35
  case "workspace": return key === null && query === undefined ? { kind } : null;
36
+ case "summary": return key === null && query === undefined ? { kind } : null;
36
37
  case "task": return key !== null && key !== "" && query === undefined ? { kind, task: key } : null;
37
38
  case "flow": {
38
39
  const flow = Number(key), card = params.get("card");
@@ -53,11 +54,34 @@ export function parseLiveRoom(name) {
53
54
  }
54
55
  return null;
55
56
  }
57
+ /** The same session, still signed in, from the cookie and the account (never this request's context): a live room's and
58
+ * a live terminal's re-proof. Null when it was signed out, ended elsewhere, revoked, rotated or re-roled. */
59
+ export function rereadBrowser(runtime, request, who) {
60
+ const again = runtime.identify(request, false);
61
+ if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
62
+ return null;
63
+ // The in-memory session may still exist after another process ends the saved session.
64
+ const hash = runtime.sessions.hashOf(again.session);
65
+ const saved = hash === null ? null : runtime.store.webSession(hash), account = runtime.store.accountOf(again.name);
66
+ if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
67
+ || saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
68
+ return null;
69
+ return again;
70
+ }
56
71
  const json = (response, status, value) => respond(response, status, "application/json", JSON.stringify(value));
57
72
  export function createLiveHandlers(runtime, live) {
58
73
  const { store, identify, liveCeiling, familyOf, taskRooms, flowRooms, clock, leadPrincipal, taskChatFocus, chatScopeOf, liveTurns, team, visible } = runtime;
59
74
  // The reader's workspace: one room for everyone, nudged by any meaningful write or a clock-bound lapse.
60
75
  const workspaceRooms = createLiveRooms(() => `${live.workspaceRevision.current()}:${live.workspaceRevision.expiresAt(clock())}`, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS });
76
+ // The reader's project states, per account AND scope: the fingerprint IS the snapshot, so each `change` carries it
77
+ // (`at`). Every delivery re-proves the viewer's scope, so a snapshot read for a wider scope (a cached one, a held
78
+ // one, one a newcomer would be handed on joining) never reaches a narrowed reader: it hears `gone` instead. Reads
79
+ // settle: a burst of writes is one read, while every signal still re-proves at once.
80
+ const summaryKey = (account, scope) => JSON.stringify([account, scope]);
81
+ const summaryRooms = createLiveRooms(key => {
82
+ const [account, scope] = JSON.parse(key);
83
+ return live.projectSummaryFor(account, scope);
84
+ }, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS, readSettleMs: WORKSPACE_SETTLE_MS });
61
85
  // A team audience is per actor (and per token): its key carries who reads, the room keeps how to read as them.
62
86
  const teamActors = new Map();
63
87
  const teamRooms = createLiveRooms(key => {
@@ -68,19 +92,7 @@ export function createLiveHandlers(runtime, live) {
68
92
  const chatViewers = new Set();
69
93
  // One agent's open view (Agents board): what it shows, keyed by the exact run.
70
94
  const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
71
- /** The same session, still signed in, from the cookie and the account (never this request's context). */
72
- const readBrowser = (request, who) => {
73
- const again = identify(request, false);
74
- if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
75
- return null;
76
- // The in-memory session may still exist after another process ends the saved session.
77
- const hash = runtime.sessions.hashOf(again.session);
78
- const saved = hash === null ? null : store.webSession(hash), account = store.accountOf(again.name);
79
- if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
80
- || saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
81
- return null;
82
- return again;
83
- };
95
+ const readBrowser = (request, who) => rereadBrowser(runtime, request, who);
84
96
  const sameBrowser = (request, who) => () => readBrowser(request, who) !== null;
85
97
  function admitted(viewer, thread) {
86
98
  let valid = false;
@@ -90,7 +102,7 @@ export function createLiveHandlers(runtime, live) {
90
102
  catch { /* Failed proof stops this room. */ }
91
103
  if (!valid) {
92
104
  viewer.connection.send(viewer.room, "gone");
93
- viewer.leave();
105
+ viewer.connection.refuse(() => viewer.leave());
94
106
  }
95
107
  return valid;
96
108
  }
@@ -184,7 +196,7 @@ export function createLiveHandlers(runtime, live) {
184
196
  if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
185
197
  return json(response, 400, { error: "room" });
186
198
  const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
187
- if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1)
199
+ if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1)
188
200
  return json(response, 400, { error: "room" });
189
201
  // Admit every room before the stream opens, with the answers each page's own stream gave.
190
202
  const joins = [];
@@ -195,6 +207,15 @@ export function createLiveHandlers(runtime, live) {
195
207
  const browser = who;
196
208
  joins.push(connection => workspaceRooms.join("workspace", { name: who.name, room: name, connection, valid: sameBrowser(request, browser) }));
197
209
  }
210
+ else if (room.kind === "summary") {
211
+ const browser = who;
212
+ const scope = live.projectScopeFor(who.name);
213
+ if (scope === null)
214
+ return json(response, 404, { error: "summary" });
215
+ const signedIn = sameBrowser(request, browser);
216
+ joins.push(connection => summaryRooms.join(summaryKey(who.name, scope), { name: who.name, room: name, connection,
217
+ valid: () => signedIn() && live.projectScopeFor(who.name) === scope }));
218
+ }
198
219
  else if (room.kind === "task") {
199
220
  const browser = who;
200
221
  const family = familyOf(room.task);
@@ -333,6 +354,7 @@ export function createLiveHandlers(runtime, live) {
333
354
  close() {
334
355
  detachChat();
335
356
  workspaceRooms.close();
357
+ summaryRooms.close();
336
358
  teamRooms.close();
337
359
  agentRooms.close();
338
360
  for (const viewer of [...chatViewers])
@@ -6,6 +6,9 @@ const PWA_ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAB1klEQVR42u
6
6
  const PWA_ICON_512 = "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAJF0lEQVR42u3VwQ0AEBBFQXU4KUD/TW0R3JzcRLJhfqYCwivDzMy+XHEEZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmJgBmZiYAZmYmAGZmJgBmZiYAZmYmAGZmJgDb1dYBOCEAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACcF+Y5Z5HKgACIAAmAAgAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgP/FBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQABMABAAARAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAFAABAAEwABEAAEwARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEAEAABABAAAQAQAAAEAAABAAAAQBAAAAQAAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAANwcgAAAIAAACAAAAgCAAAAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAgAAAIAAACAAAAgCAAAAgAAACIAC/C7Pc80gFQAAEwAQAAUAATAAQAATABAABQABMABAABMAEAAFAAEwAEAAEwAQAAUAATAAEQADwv5gACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgAkAAiAAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgACYAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAmwMQAAAEAAABAEAAABAAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAAQAAAEAAABAEAAABAAAASAJcxyzyMVAAEQABMABAABMAFAABAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAEQAAHA/2ICIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgACYACIAACIAJAAKAAJgAIAAIgAkAAoAAmAAgAAiACQACgACYACAACIAJgAAIAAJgAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIAIAACACAAAgAgAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAbg5AAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAAQBAAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABcHkAAgCAAAAgAAAIAAACAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgJmZPT4BMDMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMwEwMzMBMDMzATAzMwEwMzMBMDOzJzYBVJhD+Nnu218AAAAASUVORK5CYII=";
7
7
  const PWA_ICON_APPLE = "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABqklEQVR42u3aMQ0AIAxFwepgQgD+TSECFHQiYWjv5ylobmwcs2ThBAaHwWFwGBwGh8FhcBgcBofBkW7MparBITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3AIDsEhOASHI8IBBxxwCA7BITgEh+AQHIJDcAgOOOCAQ3AIDsEhOASH4BAcguNPu9PggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw44PPsIDsEhOASH4BAcgkNwCA444IADDjgEh+AQHIJDcAgOwSE44IDDg7EHYzjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw559hEcgkNwwAEHHHDAITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3B4MIYDDjjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOODw7OPZBw7BITgEh+AQHIJDcAgOOOCAAw44BIfgEByCQ3AIDsEhOAQHHHDAITgEh+AQHIJDcAgOwSE44IADDjgecVjbwWFwGBwGh8FhcBgcBofBYeV3AaohX51oqNRKAAAAAElFTkSuQmCC";
8
8
  const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="14" fill="#1a202c"/><rect x="28" y="28" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="47" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="66" width="44" height="8" rx="4" fill="#ebebeb"/></svg>`;
9
+ /** The icon with the attention dot (cockpit §3): a tab whose reader has something waiting shows it. Same origin, so
10
+ * the page's img-src 'self' allows it; nothing about the reader rides the asset. */
11
+ const PWA_ICON_ATTENTION_SVG = PWA_ICON_SVG.replace("</svg>", `<circle cx="78" cy="22" r="20" fill="#bb2649" stroke="#ffffff" stroke-width="6"/></svg>`);
9
12
  /** The typefaces by route: exact names only, served pre-auth like the icons. */
10
13
  const FONT_FILES = {
11
14
  "/fonts/geist-sans-400.woff2": GEIST_SANS_400,
@@ -87,6 +90,8 @@ export function serveInstallAsset(response, pathname, respond) {
87
90
  return asset("image/png", Buffer.from(PWA_ICON_APPLE, "base64"));
88
91
  if (pathname === "/icon.svg")
89
92
  return asset("image/svg+xml", PWA_ICON_SVG);
93
+ if (pathname === "/icon-attention.svg")
94
+ return asset("image/svg+xml", PWA_ICON_ATTENTION_SVG);
90
95
  if (pathname === "/icon-192.png")
91
96
  return asset("image/png", Buffer.from(PWA_ICON_192, "base64"));
92
97
  if (pathname === "/icon-512.png")