toolroll 0.9.63 → 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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  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 +50 -2
  16. package/dist/agent-fence.js +157 -10
  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 +235 -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 +197 -15
  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/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -44,6 +44,7 @@ function row(stage, domain, id, method, pattern, options = {}) {
44
44
  roleBrowser: options.roleBrowser ?? false,
45
45
  ...(options.roleRefusal === undefined ? {} : { roleRefusal: options.roleRefusal }),
46
46
  ...(options.bodyCap === undefined ? {} : { bodyCap: options.bodyCap }),
47
+ freshness: options.freshness ?? "never",
47
48
  });
48
49
  }
49
50
  const edge = (domain, id, method, path, options = {}) => row("edge", domain, id, method, path.startsWith("^") ? path : exact(path), options);
@@ -65,6 +66,9 @@ const LARGE_FORM = 1024 * 1024;
65
66
  // ---- edge: answered before the console's sign-in ---------------------------------------------------------------
66
67
  // Ordered exactly as the server checks them: health and webhooks before the Host check, the rest after it.
67
68
  edge("pages", "edge.healthz", "GET,HEAD", "/healthz", { host: "before" });
69
+ // Cockpit B1: the running console proves itself to this installation's CLI (a fresh challenge, answered with the
70
+ // instance secret and the address this connection was received on). It reveals nothing and changes nothing.
71
+ edge("pages", "edge.instance", "GET", "/__instance");
68
72
  edge("remote", "edge.telegram-hook", "POST", "/hooks/telegram", { callers: ["service"], host: "before", proof: "adapter" });
69
73
  edge("remote", "edge.flow-form", "GET,HEAD", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
70
74
  edge("remote", "edge.flow-form-send", "POST", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
@@ -83,9 +87,9 @@ edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bear
83
87
  edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
84
88
  edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
85
89
  edge("remote", "edge.teams", "POST", "/teams/messages", { callers: ["service"], proof: "adapter" });
86
- edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
90
+ edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|terminal\\.js|terminal\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
87
91
  edge("pages", "edge.workspace-style", "GET,HEAD", "^\\/assets\\/workspace-[0-9a-f]{64}\\.css$", { sample: `/assets/workspace-${"0".repeat(64)}.css` });
88
- edge("pages", "edge.install-asset", "GET", "^\\/(manifest\\.webmanifest|favicon\\.ico|icon\\.svg|icon-192\\.png|icon-512\\.png|apple-touch-icon\\.png|sw\\.js)$", { sample: "/sw.js" });
92
+ edge("pages", "edge.install-asset", "GET", "^\\/(manifest\\.webmanifest|favicon\\.ico|icon\\.svg|icon-attention\\.svg|icon-192\\.png|icon-512\\.png|apple-touch-icon\\.png|sw\\.js)$", { sample: "/sw.js" });
89
93
  edge("pages", "edge.font", "GET", "^\\/fonts\\/geist-(sans|mono)-(400|500|600)\\.woff2$", { sample: "/fonts/geist-sans-400.woff2" });
90
94
  edge("pages", "edge.desktop-health", "GET", "/desktop/health");
91
95
  edge("people", "edge.login-page", "GET", "/login");
@@ -108,6 +112,14 @@ for (const one of ["state", "changes", "ship"])
108
112
  get("tasks", `code.${one}`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING, sample: `/code/${"a".repeat(32)}/${one}` });
109
113
  // Any other coding address answers "not found" after the same access check.
110
114
  get("tasks", "code.other", "^\\/code\\/.*$", { ...CODING, sample: "/code/unknown" });
115
+ // Live terminals (docs/plans/live-terminal.md): an installation-wide operator's own, from a browser. The handlers prove
116
+ // the operator's generation, the terminal's owner and its project; the socket's upgrade is checked in terminals.ts.
117
+ const TERM = { ...free, callers: ["cookie"], project: "none", limited: "deny", ...operator("Terminals are for installation-wide operators.", "/") };
118
+ const TERMINAL = "[a-f0-9]{32}";
119
+ get("tasks", "term.page", "/term", TERM);
120
+ get("tasks", "term.state", "/term/state", TERM);
121
+ get("tasks", "term.session", `^\\/term\\/${TERMINAL}$`, { ...TERM, sample: `/term/${"a".repeat(32)}` });
122
+ get("tasks", "term.socket", `^\\/term\\/${TERMINAL}\\/socket$`, { ...TERM, sample: `/term/${"a".repeat(32)}/socket` });
111
123
  get("pages", "projects.browse", "/projects/browse", free);
112
124
  get("pages", "projects.github", "/projects/github", { ...free, callers: ["cookie"], callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "listing repositories feeds a browser session's act" },
113
125
  ...anyApprover("your login can watch — adding projects is an approver's act") });
@@ -211,8 +223,14 @@ for (const one of ["approve", "change", "revise", "complete"])
211
223
  post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
212
224
  post("settings", "settings.updates-dismiss", "/settings/updates/dismiss");
213
225
  post("settings", "settings.updates-checks", "/settings/updates/checks");
226
+ // Cockpit B2: Forget ends this session's remembered password now.
227
+ // Forget is about the caller's own session only: anyone signed in with a browser may use it (a viewer, a project-limited person).
228
+ post("settings", "session.forget-password", "/session/forget-password", { ...free, limited: "self", viewer: true, callers: ["cookie"] });
214
229
  const CODING_ACT = { scope: "step-up", project: "coding", limited: "self" };
215
230
  post("tasks", "code.start", "/code/start", CODING_ACT);
231
+ post("tasks", "term.start", "/term/start", { ...TERM, scope: "step-up" });
232
+ for (const one of ["close", "remove"])
233
+ post("tasks", `term.${one}`, `^\\/term\\/${TERMINAL}\\/${one}$`, { ...TERM, sample: `/term/${"a".repeat(32)}/${one}` });
216
234
  for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
217
235
  post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
218
236
  // Any other coding action answers "not found" after the same access check.
@@ -252,11 +270,28 @@ post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/
252
270
  post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press", ...EDIT_FLOWS });
253
271
  post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew", ...EDIT_FLOWS });
254
272
  post("flows", "flow.trigger.secret", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/secret$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/secret", ...EDIT_FLOWS });
273
+ // v126: a signed webhook's set-up (a new address, and a secret made here shown once) and who signs it: behind the password.
274
+ post("flows", "flow.trigger.setup", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/setup$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/setup", ...EDIT_FLOWS });
275
+ // v126: clear a webhook's turned-away count once it has been seen.
276
+ post("flows", "flow.trigger.dropped", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/dropped$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/dropped", ...EDIT_FLOWS });
277
+ // v126: which project a Sentry or Vercel webhook takes deliveries from: behind the password, as its secret is.
278
+ post("flows", "flow.trigger.scope", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/scope$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/scope", ...EDIT_FLOWS });
279
+ post("flows", "flow.trigger.sender", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/sender$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/sender", ...EDIT_FLOWS });
255
280
  post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share", ...EDIT_FLOWS });
256
281
  post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare", ...EDIT_FLOWS });
257
282
  post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operator("An instance operator sets budgets.", "/spend") });
258
283
  post("settings", "settings.project-concurrency", "/settings/project/concurrency");
284
+ post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
285
+ post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
259
286
  post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
287
+ // Lead sweeps (docs/plans/lead-fan-out.md): the table, and cancelling what isn't ready. The handler shows a sweep only in
288
+ // a project the reader can see, and acts only for an approver.
289
+ get("tasks", "sweep.page", "^\\/sweeps\\/[1-9][0-9]{0,9}$", { ...free, limited: "unscoped", sample: "/sweeps/1" });
290
+ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$", { limited: "unscoped", sample: "/sweeps/1/cancel-rest", ...approver("An approver cancels a sweep's work.", "/chat") });
291
+ // Passwordless (docs/plans/passwordless.md): an operator's installation setting. Turning it on takes a typed password
292
+ // (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
293
+ get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
294
+ post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
260
295
  post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
261
296
  const SETTINGS_ACT_ROLES = {
262
297
  policy: operator("An instance operator sets the policy.", "/settings/policy"),
@@ -328,7 +363,7 @@ post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task",
328
363
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
329
364
  post("tasks", "task.act.cancel", `^\\/t\\/${TASK}\\/cancel$`, { project: "task", limited: "resource", sample: "/t/one/cancel" });
330
365
  post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope", bodyCap: LARGE_FORM });
331
- post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve" });
366
+ post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve", freshness: "allowlist" });
332
367
  post("tasks", "task.act.plan", `^\\/t\\/${TASK}\\/plan$`, { project: "task", limited: "resource", sample: "/t/one/plan" });
333
368
  post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "task", limited: "resource", sample: "/t/one/plan-edit" });
334
369
  post("tasks", "task.act.next", `^\\/t\\/${TASK}\\/next$`, { project: "task", limited: "resource", sample: "/t/one/next" });
@@ -361,11 +396,15 @@ for (const one of ["identity", "about", "promise/cancel", "on"])
361
396
  post("chat", "chat.config", "/chat/config", { scope: "step-up" });
362
397
  for (const one of ["mint", "follow", "end", "stop"])
363
398
  post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
399
+ // Remove a message still waiting in the person's lead queue (docs/plans/lead-message-queue.md): their own, while it waits.
400
+ post("chat", "chat.queued-remove", "^\\/chat\\/queued\\/[1-9][0-9]{0,14}\\/remove$", { callers: ["cookie"], sample: "/chat/queued/1/remove" });
401
+ // Confirming a proposal may approve a scope (scope_approve): that one act takes the same standard-approval freshness as
402
+ // /t/:id/approve (chat-actions.ts scopes it); every other operation it confirms still needs a password typed now.
364
403
  for (const one of ["confirm", "dismiss"])
365
- post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}` });
404
+ post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}`, ...(one === "confirm" ? { freshness: "allowlist" } : {}) });
366
405
  for (const one of ["confirm", "dismiss"])
367
406
  post("chat", `coordinator.proposal.${one}`, `^\\/proposals\\/${ID}\\/${one}$`, { sample: `/proposals/1/${one}` });
368
- post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation" });
407
+ post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation", freshness: "allowlist" });
369
408
  post("chat", "chat.file", "^\\/chat\\/file\\/[0-9a-f]{32}$", { scope: "step-up", sample: `/chat/file/${"a".repeat(32)}` });
370
409
  post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sample: "/chat/ack/1" });
371
410
  for (const one of ["prepare", "preview", "import", "save"])
@@ -373,6 +412,8 @@ for (const one of ["prepare", "preview", "import", "save"])
373
412
  post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
374
413
  for (const one of ["note", "comment", "revise", "draft-repair", "checks", "add-tests"])
375
414
  post("tasks", `run.act.${one}`, `^\\/r\\/${ID}\\/${one}$`, { project: "run", limited: "resource", sample: `/r/1/${one}` });
415
+ // Cockpit E: reveal a running attempt's proved checkout in Finder (nothing is launched in it).
416
+ post("tasks", "run.act.reveal", `^\\/r\\/${ID}\\/reveal$`, { project: "run", limited: "resource", callers: ["cookie"], sample: "/r/1/reveal", ...approver("Only an approver can show an agent's files.") });
376
417
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
377
418
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
378
419
  /** The sole route table, in match order. */
@@ -1,3 +1,5 @@
1
+ import type { FreshFacts } from '../scope.js';
2
+ import type { createPersonalQueueRuntime } from '../lead-queue-runtime.js';
1
3
  import type { FlowRooms } from '../flow-live.js';
2
4
  import type { IntegrationMonitor } from '../integrations.js';
3
5
  import type { OidcSettings } from '../oidc.js';
@@ -7,7 +9,8 @@ import type { TaskRooms } from '../task-live.js';
7
9
  /** Captured server state and shared guards. Getters preserve live state across requests. */
8
10
  import { type IncomingMessage, type ServerResponse } from "node:http";
9
11
  import { type AssignmentSnapshot } from '../assignment.js';
10
- import { type BrowserPhoneCard, type BrowserUpdates } from '../browser-workspace.js';
12
+ import { type BrowserPhoneCard, type BrowserResultView, type BrowserTaskTab, type BrowserUpdates } from '../browser-workspace.js';
13
+ import type { Html } from '../html.js';
11
14
  import { CodingWorkspace } from '../coding-workspace.js';
12
15
  import { type FormFieldOf, type FormView } from "../contracts/console-api.js";
13
16
  import type { DemoLead } from "../demo.js";
@@ -93,7 +96,7 @@ export interface ServerRuntime {
93
96
  repos: string[];
94
97
  roots: readonly string[];
95
98
  };
96
- taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean) => void;
99
+ taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, page?: TaskPageExtras) => void;
97
100
  runVisible: (run: Run) => boolean;
98
101
  runIsLive: (run: Pick<Run, "outcome" | "leaseId" | "taskRef">) => boolean;
99
102
  options: ServeOptions;
@@ -134,6 +137,8 @@ export interface ServerRuntime {
134
137
  generation: number;
135
138
  }, csrf: string) => TeamResponse;
136
139
  team: ReturnType<typeof createTeamRuntime>;
140
+ /** The personal lead queue (lead-queue-runtime.ts): a message sent while the person's lead is busy waits here. */
141
+ personalQueue: ReturnType<typeof createPersonalQueueRuntime>;
137
142
  teamChatProvider: TeamChatProviderResolver;
138
143
  pullRequestTargetOf: (runId: number) => string | null;
139
144
  chatEnablement: () => ChatEnablement;
@@ -176,6 +181,8 @@ export interface ServerRuntime {
176
181
  settingsUpdates: (actor: string, csrf: string) => BrowserUpdates | null;
177
182
  phoneSetup: (who: Who) => BrowserPhoneCard | undefined;
178
183
  authenticateApprover: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
184
+ /** A credential act (passwordless.md's closed list): a password typed now, never a session or a remembered one. */
185
+ authenticateCredential: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
179
186
  projectOf: (who: Who, request: IncomingMessage) => string | null | undefined;
180
187
  bustBadge: () => void;
181
188
  revisionDestination: (child: string, back: string) => string;
@@ -223,6 +230,8 @@ export interface ServerRuntime {
223
230
  }>;
224
231
  linkKey: (code: string) => string;
225
232
  fromThisComputer: (request: IncomingMessage) => boolean;
233
+ /** Cockpit B2: the request's live password-freshness facts (null off a browser session). */
234
+ freshFactsOf: (request: IncomingMessage, who: Who | null) => FreshFacts | null;
226
235
  passwordAllowed: (name: string) => boolean;
227
236
  arrival: (request: IncomingMessage) => {
228
237
  agent: string | null;
@@ -236,6 +245,11 @@ export interface ServerRuntime {
236
245
  intent: SsoIntent;
237
246
  returnTo: string;
238
247
  expires: number;
248
+ startedAt: number;
249
+ session: {
250
+ hash: string;
251
+ generation: number;
252
+ } | null;
239
253
  }>;
240
254
  ssoHandoffs: Map<string, {
241
255
  claims: OidcClaims;
@@ -243,6 +257,11 @@ export interface ServerRuntime {
243
257
  intent: SsoIntent;
244
258
  returnTo: string;
245
259
  expires: number;
260
+ checkedAt: number | null;
261
+ session: {
262
+ hash: string;
263
+ generation: number;
264
+ } | null;
246
265
  }>;
247
266
  joinSourceOf: (request: IncomingMessage) => string;
248
267
  signInBudget: SourceBudget;
@@ -286,3 +305,15 @@ export interface ServerRuntime {
286
305
  reading: boolean;
287
306
  }>;
288
307
  }
308
+ /** What the task page's GET adds (docs/plans/one-page-task-and-agent.md): the tab its address names, and the Result tab's
309
+ * selected result (its no-script HTML and its view), whether it is the task's current one. Refusals re-render without. */
310
+ export type TaskPageExtras = {
311
+ tab?: BrowserTaskTab;
312
+ result?: {
313
+ html: Html;
314
+ selected: NonNullable<BrowserResultView["selected"]>;
315
+ current: boolean;
316
+ } | null;
317
+ /** The result runs the attempt picker may offer (the Result tab would show each); absent: every finished result run. */
318
+ admissible?: ReadonlySet<number>;
319
+ };
@@ -14,6 +14,11 @@ export declare const SIGN_IN_LINK_MS: number;
14
14
  /** A cookie idles out after half a day and dies outright after a week. */
15
15
  export declare const SESSION_IDLE_MS: number;
16
16
  export declare const SESSION_ABSOLUTE_MS: number;
17
+ /** A session's lifetime, the one reading: within its idle and absolute limits (sign-in lookup and every act use it). */
18
+ export declare const sessionWithinLifetime: (session: {
19
+ lastSeen: number;
20
+ createdAt: number;
21
+ }, now: number) => boolean;
17
22
  /** An approval nonce is a rendered form, not a standing right — it ages out fast. */
18
23
  export declare const NONCE_TTL_MS: number;
19
24
  export declare const NONCE_CAP = 500;
@@ -37,6 +42,15 @@ export type Session = {
37
42
  sso?: {
38
43
  at: number;
39
44
  };
45
+ /** Cockpit B2: when a password was last verified on this session (in memory only: a restart forgets it). */
46
+ passwordAt?: number;
47
+ /** Cockpit B2: when the identity provider last re-checked this person on request (a step-up's re-authentication).
48
+ * Only this, or a password, starts a freshness window; an ordinary sign-in never does. In memory only. */
49
+ ssoReauthAt?: number;
50
+ /** Cockpit B2: an explicit provider re-check not yet spent: stands in for ONE step-up (consumed by it). In memory only. */
51
+ ssoNowAt?: number;
52
+ /** Forget: nothing verified before this counts as fresh. */
53
+ freshFloor?: number;
40
54
  /** v101: the browser and address it signed in from, for the person to recognise it. */
41
55
  agent?: string | null;
42
56
  address?: string | null;
@@ -121,12 +135,22 @@ export declare class PersistentSessions extends Map<string, Session> {
121
135
  static hash(id: string): string;
122
136
  set(id: string, session: Session): this;
123
137
  delete(id: string): boolean;
124
- /** Keep a changed session (its project, its provider check); `seen` only once a minute. */
138
+ /**
139
+ * Keep a changed session (its project, its provider check); `seen` only once a minute. Only ever an update: a session
140
+ * ended elsewhere (another process, Sessions) is dropped here, never written back by its cached copy (live terminals
141
+ * review, rounds 2–3, finding 1).
142
+ */
125
143
  persist(session: Session, seen?: boolean): void;
144
+ /** Whether this exact session is still saved in the database (ended from another process, it isn't): read at the act. */
145
+ savedNow(session: Session): boolean;
146
+ /** Whether this cookie's session is still saved (one ended in another process is not). */
147
+ stillSaved(id: string): boolean;
126
148
  /** A cookie from before a restart, if its session is still kept. */
127
149
  load(id: string): Session | null;
128
150
  /** End one session by its kept hash (from the Sessions page). */
129
151
  endByHash(idHash: string): void;
152
+ /** Whether this exact session is still signed in here (not signed out, ended from Sessions, expired or replaced). */
153
+ holds(session: Session): boolean;
130
154
  hashOf(session: Session): string | null;
131
155
  }
132
156
  /** The console's server, and the one-time sign-in link `up` opens: a path on this server, or null for no such approver.
@@ -26,6 +26,8 @@ export const SIGN_IN_LINK_MS = 10 * 60_000;
26
26
  /** A cookie idles out after half a day and dies outright after a week. */
27
27
  export const SESSION_IDLE_MS = 12 * 60 * 60_000;
28
28
  export const SESSION_ABSOLUTE_MS = 7 * 24 * 60 * 60_000;
29
+ /** A session's lifetime, the one reading: within its idle and absolute limits (sign-in lookup and every act use it). */
30
+ export const sessionWithinLifetime = (session, now) => now - session.lastSeen <= SESSION_IDLE_MS && now - session.createdAt <= SESSION_ABSOLUTE_MS;
29
31
  /** An approval nonce is a rendered form, not a standing right — it ages out fast. */
30
32
  export const NONCE_TTL_MS = 15 * 60_000;
31
33
  export const NONCE_CAP = 500;
@@ -55,22 +57,32 @@ export class PersistentSessions extends Map {
55
57
  set(id, session) {
56
58
  super.set(id, session);
57
59
  this.ids.set(session, id);
58
- this.persist(session);
60
+ this.savedAt.set(session, Date.now());
61
+ this.db.saveWebSession({ idHash: PersistentSessions.hash(id), account: session.name, csrf: session.csrf, role: session.role, generation: session.generation, createdAt: session.createdAt,
62
+ lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null, agent: session.agent ?? null, address: session.address ?? null });
59
63
  return this;
60
64
  }
61
65
  delete(id) {
62
66
  this.db.dropWebSession(PersistentSessions.hash(id));
63
67
  return super.delete(id);
64
68
  }
65
- /** Keep a changed session (its project, its provider check); `seen` only once a minute. */
69
+ /**
70
+ * Keep a changed session (its project, its provider check); `seen` only once a minute. Only ever an update: a session
71
+ * ended elsewhere (another process, Sessions) is dropped here, never written back by its cached copy (live terminals
72
+ * review, rounds 2–3, finding 1).
73
+ */
66
74
  persist(session, seen = false) {
67
75
  const id = this.ids.get(session);
68
76
  if (id === undefined || (seen && Date.now() - (this.savedAt.get(session) ?? 0) < 60_000))
69
77
  return;
70
78
  this.savedAt.set(session, Date.now());
71
- this.db.saveWebSession({ idHash: PersistentSessions.hash(id), account: session.name, csrf: session.csrf, role: session.role, generation: session.generation, createdAt: session.createdAt,
72
- lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null, agent: session.agent ?? null, address: session.address ?? null });
79
+ if (!this.db.updateWebSession({ idHash: PersistentSessions.hash(id), lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null }))
80
+ super.delete(id);
73
81
  }
82
+ /** Whether this exact session is still saved in the database (ended from another process, it isn't): read at the act. */
83
+ savedNow(session) { const hash = this.hashOf(session); return hash !== null && this.db.webSession(hash) !== null; }
84
+ /** Whether this cookie's session is still saved (one ended in another process is not). */
85
+ stillSaved(id) { return this.db.webSession(PersistentSessions.hash(id)) !== null; }
74
86
  /** A cookie from before a restart, if its session is still kept. */
75
87
  load(id) {
76
88
  const row = this.db.webSession(PersistentSessions.hash(id));
@@ -90,5 +102,7 @@ export class PersistentSessions extends Map {
90
102
  super.delete(id);
91
103
  this.db.dropWebSession(idHash);
92
104
  }
105
+ /** Whether this exact session is still signed in here (not signed out, ended from Sessions, expired or replaced). */
106
+ holds(session) { const id = this.ids.get(session); return id !== undefined && super.get(id) === session; }
93
107
  hashOf(session) { const id = this.ids.get(session); return id === undefined ? null : PersistentSessions.hash(id); }
94
108
  }
@@ -1,3 +1,6 @@
1
+ import { PASSWORDLESS_TRADE, passwordlessSetting, setPasswordless } from "../passwordless.js";
2
+ import { ACP_AGENTS, isKeyedProvider } from "../acp-agents.js";
3
+ import { LIVE_VIEW_CONSENT, liveViewConsentDigest, setLiveCommands } from "../live-detail.js";
1
4
  import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
2
5
  import { galleryTemplateOf } from "../flow-gallery.js";
3
6
  import { finishGoogleConsent, GOOGLE_CALLBACK } from "../google-mail.js";
@@ -30,7 +33,7 @@ import { PACKAGE_VERSION } from "../version.js";
30
33
  import { handlersOf } from './handler-registry.js';
31
34
  import { refuse, screen } from "./chrome.js";
32
35
  import { HANDOFF_STYLE, redirect, respond, safeReturn, signInSpent, startedHere } from "./http.js";
33
- import { html, htmlString, joinHtml, postForm } from "../html.js";
36
+ import { html, htmlString, joinHtml, postForm, reauth } from "../html.js";
34
37
  import { browserCaller } from "./session.js";
35
38
  /** settings handlers, moved without changing their route bodies. */
36
39
  import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
@@ -76,6 +79,7 @@ import { openRouterPickerScript } from "../openrouter-models.js";
76
79
  import { policyHtml } from "../policy-ui.js";
77
80
  import { checkPolicy, parseList, policyParts } from "../policy.js";
78
81
  import { parseProjectConcurrency, saveProjectConcurrency } from "../project-concurrency.js";
82
+ import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
79
83
  import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
80
84
  import { deleteProject, holdingsWords } from "../project-delete.js";
81
85
  import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
@@ -122,7 +126,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
122
126
  import { chatReturnWithSaid, goOutside } from "./http.js";
123
127
  import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
124
128
  export function createSettingsHandlers(runtime) {
125
- const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
129
+ const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
126
130
  // v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
127
131
  async function spendPage(ctx) {
128
132
  const { url, who, response, now } = ctx;
@@ -461,6 +465,40 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
461
465
  const body = ssoSettingsHtml({ settings, redirect: origin === null ? null : `${origin}${SSO_CALLBACK}`, projects, linked: settings !== null && store.ssoIdentitiesOf(who.name).some(one => one.issuer.replace(/\/+$/, "") === settings.issuer) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
462
466
  return sendScreen(response, 200, screen("Sign-in", html `<p><a href="/settings">Settings</a></p><h1>Sign-in</h1>${body}`, { chrome: chromeFor(project, "settings") }));
463
467
  }
468
+ /** Passwordless (docs/plans/passwordless.md): the switch, and the trade in plain words. */
469
+ async function passwordlessPage(ctx) {
470
+ const { url, response, project } = ctx;
471
+ browserCaller(ctx.who);
472
+ const setting = passwordlessSetting(store);
473
+ const said = url.searchParams.get("said"), problem = url.searchParams.get("problem");
474
+ const note = problem ? html `<p class="problem" role="alert">${problem}</p>` : said ? html `<p role="status">${said}</p>` : "";
475
+ const since = setting.at === null ? "" : html ` <span class="meta">(${setting.on ? "on" : "off"} since ${new Date(setting.at).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}, by ${setting.setBy ?? "someone"})</span>`;
476
+ const control = setting.on
477
+ ? postForm("/settings/passwordless", html `<button class="secondary">Turn Passwordless off</button>`, { hidden: { action: "off" } })
478
+ : postForm("/settings/passwordless", html `<label>Your password<input type="password" name="password" autocomplete="current-password" required></label><button>Turn Passwordless on</button>`, { attrs: { class: "card approve-form" }, hidden: { action: "on" } });
479
+ return sendScreen(response, 200, screen("Passwordless", html `<p><a href="/settings">Settings</a></p><h1>Passwordless</h1>${note}\
480
+ <p>When it's on, signing in is the only password: approvals, settings, budgets, signing a mode and starting a terminal no longer ask again — in the browser, on the phone, and in this computer's <code>toolroll</code> command (through its remembered login).</p>\
481
+ <p class="meta">${PASSWORDLESS_TRADE}</p>\
482
+ <p><strong>Passwordless is ${setting.on ? "on" : "off"}.</strong>${since}</p>${control}`, { chrome: chromeFor(project, "settings") }));
483
+ }
484
+ async function passwordlessSave(ctx) {
485
+ const { response, now, posted } = ctx;
486
+ const who = browserCaller(ctx.who);
487
+ const body = readForm(posted, CONSOLE_FORMS.passwordless);
488
+ const back = (key, words) => redirect(response, `/settings/passwordless?${key}=${encodeURIComponent(words)}`);
489
+ const action = body.get("action");
490
+ if (action === "off") {
491
+ setPasswordless(store, false, who.name, now);
492
+ return back("said", "Passwordless is off. Step-ups ask for your password again.");
493
+ }
494
+ if (action !== "on")
495
+ return back("problem", "Choose on or off.");
496
+ // Turning it on is a credential act: a password typed now (or a one-use provider re-check), never a session.
497
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
498
+ return back("problem", "Enter your Toolroll password to turn Passwordless on.");
499
+ setPasswordless(store, true, who.name, now);
500
+ return back("said", "Passwordless is on. Signing in is the only password now.");
501
+ }
464
502
  async function knowledgePage(ctx) {
465
503
  const { url, who, response, project } = ctx;
466
504
  const projects = consoleProjects();
@@ -600,13 +638,14 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
600
638
  };
601
639
  const providerKeys = who.role !== "approver"
602
640
  ? null
603
- : await Promise.all(PROVIDER_IDS.map(async (provider) => ({
641
+ // ACP agents sign in their own way; Toolroll keeps no key for them, so the key card lists only the others.
642
+ : await Promise.all(PROVIDER_IDS.filter(isKeyedProvider).map(async (provider) => ({
604
643
  provider,
605
644
  envName: PROVIDER_KEY_ENV[provider],
606
645
  ...keyStatus(provider, providerHome),
607
646
  connection: await connectionCheck(provider, url.searchParams.get("check-connection") === provider),
608
647
  ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
609
- mode: readAuthMode(provider, providerHome),
648
+ mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
610
649
  subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
611
650
  })));
612
651
  const telegramConfigured = loadBotToken(process.env, options.telegramTokenFile) !== null;
@@ -656,6 +695,18 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
656
695
  return redirect(response, "/work");
657
696
  }
658
697
  // The update notice is dismissed per browser and per version: a newer release shows it again.
698
+ // Cockpit B2: Forget — nothing verified on this session before now counts as fresh (a CSRF-checked post, like every act).
699
+ async function forgetPassword(ctx) {
700
+ const { who, response, posted } = ctx;
701
+ const body = readForm(posted, CONSOLE_FORMS.forgetPassword);
702
+ if (who.via === "cookie") {
703
+ who.session.freshFloor = Date.now();
704
+ delete who.session.passwordAt;
705
+ delete who.session.ssoReauthAt;
706
+ delete who.session.ssoNowAt;
707
+ }
708
+ return redirect(response, safeReturn(body.get("return") ?? "/"));
709
+ }
659
710
  async function updatesDismiss(ctx) {
660
711
  const { who, response, posted } = ctx;
661
712
  const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
@@ -808,6 +859,51 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
808
859
  store.bumpWake();
809
860
  return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
810
861
  }
862
+ // Settings → Project → Live view (live-detail.ts): an approver for the project shows commands with their password,
863
+ // or hides them in one step; the ledger keeps each change. It applies from the next run.
864
+ async function projectLiveView(ctx) {
865
+ const { who, response, now, posted } = ctx;
866
+ const body = readForm(posted, CONSOLE_FORMS.projectLiveView);
867
+ const repo = body.get("repo") ?? "";
868
+ if (!consoleProjects().includes(repo) || !visible(repo))
869
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
870
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
871
+ return refuse(response, who, 403, "An approver for this project changes the live view.", "/settings/project");
872
+ const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
873
+ const act = body.get("act");
874
+ if (act === "off")
875
+ return back("said", setLiveCommands(store, { repo, commands: false, by: who.name, via: "the console" }, now).said);
876
+ if (act !== "on")
877
+ return back("problem", "That change isn't available.");
878
+ // Single use, for this person and exactly this change: a replayed or copied form changes nothing.
879
+ if (!consumeApprovalNonce(body.get("nonce") ?? "", who.name, LIVE_VIEW_CONSENT, liveViewConsentDigest(repo)))
880
+ return back("problem", "That form was already used or is out of date. Try again from this page.");
881
+ if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
882
+ return back("problem", "Enter your Toolroll password to show commands.");
883
+ return back("said", setLiveCommands(store, { repo, commands: true, by: who.name, via: "the console" }, now).said);
884
+ }
885
+ // Settings → Project → Network (egress.ts): what the project's builds may reach. An approver for the project changes it
886
+ // with their password typed now; the ledger keeps before → after. New scopes use it; approved ones keep theirs.
887
+ async function projectNetwork(ctx) {
888
+ const { who, response, now, posted } = ctx;
889
+ const body = readForm(posted, CONSOLE_FORMS.projectNetwork);
890
+ const repo = body.get("repo") ?? "";
891
+ if (!consoleProjects().includes(repo) || !visible(repo))
892
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
893
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
894
+ return refuse(response, who, 403, "An approver for this project changes what its builds may reach.", "/settings/project");
895
+ const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#network`);
896
+ const hosts = (body.get("hosts") ?? "").split(/[\s,]+/).filter(Boolean).map(normalizeHost);
897
+ if (hosts.some(one => one === null))
898
+ return back("problem", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path. Nothing changed.");
899
+ if (hosts.length > MAX_NETWORK_HOSTS)
900
+ return back("problem", `A project lists at most ${MAX_NETWORK_HOSTS} hosts. Nothing changed.`);
901
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
902
+ return back("problem", "Enter your Toolroll password to change what builds may reach.");
903
+ const changed = store.setProjectNetwork(repo, { mode: body.get("mode") === "open" ? "open" : "allowlist", hosts: hosts, loopback: body.get("local") === "any" ? "any" : "proxy-only" }, who.name, now);
904
+ const same = JSON.stringify(changed.before) === JSON.stringify(changed.after);
905
+ return back("said", same ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep the network they were approved with.");
906
+ }
811
907
  // Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
812
908
  async function projectDelete(ctx) {
813
909
  const { response, now, posted } = ctx;
@@ -917,7 +1013,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
917
1013
  const found = await providerFor(before.issuer);
918
1014
  return found.ok ? back("said", `${before.label} answers: it's ready for people to sign in.`) : back("problem", found.said);
919
1015
  }
920
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1016
+ // Sign-in methods and the identity provider are the login itself: a credential act, a password typed now.
1017
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
921
1018
  return back("problem", "Enter your Toolroll password to change sign-in.");
922
1019
  if (action === "remove") {
923
1020
  removeSsoSettings(options.configDir);
@@ -1611,7 +1708,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1611
1708
  const nonceKey = `project-instructions:${project}`;
1612
1709
  if (step === "preview") {
1613
1710
  const nonce = mintApprovalNonce(who.name, nonceKey, preview.fingerprint);
1614
- return sendScreen(response, 200, screen("Review agent instructions", html `<h1>Review agent instructions</h1><p>${preview.plan.skillPath}</p><pre class="recap">${preview.content}</pre>${postForm("/control/instructions-approve", html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><button>Add these instructions</button>`, { hidden: { repo: project, nonce, fingerprint: preview.fingerprint } })}`, { chrome: chromeFor(project, "settings") }));
1711
+ return sendScreen(response, 200, screen("Review agent instructions", html `<h1>Review agent instructions</h1><p>${preview.plan.skillPath}</p><pre class="recap">${preview.content}</pre>${postForm("/control/instructions-approve", html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button>Add these instructions</button>`, { hidden: { repo: project, nonce, fingerprint: preview.fingerprint } })}`, { chrome: chromeFor(project, "settings") }));
1615
1712
  }
1616
1713
  if (who.via === "cookie" && !consumeApprovalNonce(body.get("nonce") ?? "", who.name, nonceKey, body.get("fingerprint") ?? ""))
1617
1714
  return refuse(response, who, 409, "This instruction preview expired. Review it again.", "/control");
@@ -1655,7 +1752,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1655
1752
  savePrimary(dir, "slack");
1656
1753
  return redirect(response, "/settings/slack");
1657
1754
  }
1658
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1755
+ // Connecting or pairing opens a way to act from Slack: a credential act. Unpairing or disconnecting only closes one.
1756
+ if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
1659
1757
  return show("Enter your Toolroll password to change Slack access.", 403);
1660
1758
  const generation = store.accountOf(who.name).generation;
1661
1759
  if (action === "connect") {
@@ -1758,6 +1856,11 @@ ${postForm("/settings/chat-approval/save", html `\
1758
1856
  return show("Submit one value for each field.");
1759
1857
  if (botId === null)
1760
1858
  return show("Connect the Telegram bot first.", 409);
1859
+ // Pairing a phone is a credential act (a typed password, always); unpairing only closes a way in.
1860
+ if (op === "unpair" && authenticateApprover(who, body.get("password") ?? "").ok) {
1861
+ store.unpairTelegram(botId, who.name, now);
1862
+ return redirect(response, "/settings/telegram");
1863
+ }
1761
1864
  const pairingIdentity = authenticateAccount(store, who.name, body.get("password") ?? "");
1762
1865
  if (!pairingIdentity.ok || pairingIdentity.role !== "approver" || pairingIdentity.generation !== who.session.generation)
1763
1866
  return show("Enter your Toolroll password to change your phone pairing.", 403);
@@ -1788,7 +1891,7 @@ ${postForm("/settings/chat-approval/save", html `\
1788
1891
  savePrimary(dir, "teams");
1789
1892
  return redirect(response, "/settings/teams");
1790
1893
  }
1791
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1894
+ if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
1792
1895
  return show("Enter your Toolroll password to change Teams access.", 403);
1793
1896
  const generation = store.accountOf(who.name).generation;
1794
1897
  if (action === "connect") {
@@ -1845,7 +1948,7 @@ ${postForm("/settings/chat-approval/save", html `\
1845
1948
  savePrimary(dir, "discord");
1846
1949
  return redirect(response, "/settings/discord");
1847
1950
  }
1848
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1951
+ if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
1849
1952
  return show("Enter your Toolroll password to change Discord access.", 403);
1850
1953
  const generation = store.accountOf(who.name).generation;
1851
1954
  if (action === "connect") {
@@ -1984,6 +2087,8 @@ ${postForm("/settings/chat-approval/save", html `\
1984
2087
  const provider = body.get("provider") ?? "";
1985
2088
  if (!isProviderId(provider))
1986
2089
  return refuse(response, who, 400, "unknown provider", "/settings");
2090
+ if (!isKeyedProvider(provider))
2091
+ return refuse(response, who, 400, `${ACP_AGENTS[provider].name} signs in its own way — Toolroll keeps no key for it`, "/settings");
1987
2092
  if (op === "clear") {
1988
2093
  const cleared = clearProviderKey(provider, providerHome);
1989
2094
  const clearedMode = readAuthMode(provider, providerHome);
@@ -2167,7 +2272,8 @@ ${modeWords(terms)
2167
2272
  return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
2168
2273
  }
2169
2274
  const token = body.get("token") ?? "";
2170
- if (!authenticateApprover(who, token).ok) {
2275
+ // A mode that lets a chat app approve opens a way to act: a credential act. Any other mode is an ordinary one.
2276
+ if (!(body.get("chat-approve") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
2171
2277
  return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
2172
2278
  }
2173
2279
  store.signMode({
@@ -2353,6 +2459,8 @@ ${modeWords(terms)
2353
2459
  "settings.backups": backupsPage,
2354
2460
  "settings.data": dataPage,
2355
2461
  "settings.sign-in": signInPage,
2462
+ "settings.passwordless": passwordlessPage,
2463
+ "settings.passwordless-send": passwordlessSave,
2356
2464
  "settings.lead": leadPage,
2357
2465
  "settings.teams": teamsPage,
2358
2466
  "settings.discord": discordPage,
@@ -2366,12 +2474,15 @@ ${modeWords(terms)
2366
2474
  "settings.page": settingsHome,
2367
2475
  "provider.resume": providerResume,
2368
2476
  "settings.updates-dismiss": updatesDismiss,
2477
+ "session.forget-password": forgetPassword,
2369
2478
  "settings.updates-checks": updatesChecks,
2370
2479
  "settings.skills-revise": skillsRevise,
2371
2480
  "settings.skills-import": skillsAct("import"),
2372
2481
  "settings.skills-change": skillsAct("change"),
2373
2482
  "spend.budget": spendBudget,
2374
2483
  "settings.project-concurrency": projectConcurrency,
2484
+ "settings.project-live-view": projectLiveView,
2485
+ "settings.project-network": projectNetwork,
2375
2486
  "settings.project-delete": projectDelete,
2376
2487
  "settings.policy-send": policySave,
2377
2488
  "settings.approval-send": approvalSave,
@@ -0,0 +1,4 @@
1
+ import type { ServerRuntime } from "./runtime.js";
2
+ export declare function createSweepHandlers(runtime: ServerRuntime): {
3
+ registrations: import("./handler-registry.js").Registration[];
4
+ };