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
@@ -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,13 +79,15 @@ 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";
82
86
  import { changeLearning } from "../project-learning.js";
83
87
  import { listDecisions, recordDecision, retireDecision, searchMemory } from "../project-memory.js";
84
88
  import { changeSkills, githubSkill, importSkill, reviseSkillTest, skillsView, testSkill } from "../project-skills.js";
85
- import { addToolTo, catalogTool, localAppOf, removeToolFrom, setToolSecret, splitCommandLine, testToolOf } from "../project-tools.js";
89
+ import { signedIn } from "../oauth-run-token.js";
90
+ import { addToolTo, catalogTool, localAppOf, removeToolFrom, setToolSecret, splitCommandLine, testToolOf, toolSecretStorage } from "../project-tools.js";
86
91
  import { liftAuthPause } from "../provider-auth.js";
87
92
  import { isProviderId, PROVIDER_IDS, validateSpec, validModelId } from "../provider.js";
88
93
  import { checkPublishing, MERGE_METHODS, saveMergeSettings, savePublishing } from '../pull-request-flow.js';
@@ -121,7 +126,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
121
126
  import { chatReturnWithSaid, goOutside } from "./http.js";
122
127
  import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
123
128
  export function createSettingsHandlers(runtime) {
124
- 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;
125
130
  // v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
126
131
  async function spendPage(ctx) {
127
132
  const { url, who, response, now } = ctx;
@@ -350,7 +355,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
350
355
  if (!options.configDir)
351
356
  return refuse(response, who, 403, "An installation approver sees integrations.", "/settings");
352
357
  const list = integrations.list();
353
- return sendScreen(response, 200, screen("Integrations", html `<p><a href="/settings">Settings</a></p><h1>Integrations</h1>${(integrationsHtml(list, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), checking: integrations.checking }))}`, { chrome: chromeFor(project, "settings") }));
358
+ return sendScreen(response, 200, screen("Integrations", html `<p><a href="/settings">Settings</a></p><h1>Integrations</h1>${(integrationsHtml(list, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), checking: integrations.checking, secrets: toolSecretStorage() }))}`, { chrome: chromeFor(project, "settings") }));
354
359
  }
355
360
  // v104: Settings → Monitoring, where the audit stream and traces go. An instance operator's page.
356
361
  async function monitoringPage(ctx) {
@@ -460,6 +465,40 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
460
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") });
461
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") }));
462
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
+ }
463
502
  async function knowledgePage(ctx) {
464
503
  const { url, who, response, project } = ctx;
465
504
  const projects = consoleProjects();
@@ -599,13 +638,14 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
599
638
  };
600
639
  const providerKeys = who.role !== "approver"
601
640
  ? null
602
- : 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) => ({
603
643
  provider,
604
644
  envName: PROVIDER_KEY_ENV[provider],
605
645
  ...keyStatus(provider, providerHome),
606
646
  connection: await connectionCheck(provider, url.searchParams.get("check-connection") === provider),
607
647
  ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
608
- mode: readAuthMode(provider, providerHome),
648
+ mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
609
649
  subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
610
650
  })));
611
651
  const telegramConfigured = loadBotToken(process.env, options.telegramTokenFile) !== null;
@@ -624,7 +664,12 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
624
664
  }, who.role === "approver" && csrf !== "" ? (() => {
625
665
  // v87: the mail server flows send email through; the password is never shown back.
626
666
  // v89: where Inbox triggers read it, and a connected Google account (its client secret is never shown back either).
627
- const email = readEmailSettings(options.configDir ?? null), google = readGoogleMail(options.configDir ?? null), origin = consoleOrigin(request.headers.host);
667
+ const email = readEmailSettings(options.configDir ?? null), google = (() => { try {
668
+ return readGoogleMail(options.configDir ?? null);
669
+ }
670
+ catch {
671
+ return null;
672
+ } })(), origin = consoleOrigin(request.headers.host);
628
673
  const googleView = { connected: googleConnected(options.configDir ?? null)?.address ?? null, clientId: google?.clientId ?? "", redirect: origin === null ? null : `${origin}${GOOGLE_CALLBACK}` };
629
674
  return email === null ? { set: false, host: "", port: 587, secure: false, user: "", from: "", imapHost: "", imapPort: 993, google: googleView }
630
675
  : { set: true, host: email.host, port: email.port, secure: email.secure, user: email.user, from: email.from, imapHost: email.imap?.host ?? "", imapPort: email.imap?.port ?? 993, google: googleView };
@@ -650,6 +695,18 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
650
695
  return redirect(response, "/work");
651
696
  }
652
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
+ }
653
710
  async function updatesDismiss(ctx) {
654
711
  const { who, response, posted } = ctx;
655
712
  const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
@@ -802,6 +859,51 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
802
859
  store.bumpWake();
803
860
  return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
804
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
+ }
805
907
  // Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
806
908
  async function projectDelete(ctx) {
807
909
  const { response, now, posted } = ctx;
@@ -911,7 +1013,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
911
1013
  const found = await providerFor(before.issuer);
912
1014
  return found.ok ? back("said", `${before.label} answers: it's ready for people to sign in.`) : back("problem", found.said);
913
1015
  }
914
- 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)
915
1018
  return back("problem", "Enter your Toolroll password to change sign-in.");
916
1019
  if (action === "remove") {
917
1020
  removeSsoSettings(options.configDir);
@@ -1397,6 +1500,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1397
1500
  const secret = body.get("secret") ?? "";
1398
1501
  if (!tool.spec.secrets.some(one => one.name === secret))
1399
1502
  return back("problem", "Choose one of this tool's secrets.");
1503
+ if (signedIn(tool.spec))
1504
+ return back("problem", `${name} is connected by signing in. Sign in again from Connect to renew it.`, `#tool-${name}`);
1400
1505
  setToolSecret(repo, name, secret, body.get("value") ?? "", toolHome);
1401
1506
  return back("said", `Saved ${secret} for ${name}. It is never shown again.`, `#tool-${name}`);
1402
1507
  }
@@ -1603,7 +1708,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1603
1708
  const nonceKey = `project-instructions:${project}`;
1604
1709
  if (step === "preview") {
1605
1710
  const nonce = mintApprovalNonce(who.name, nonceKey, preview.fingerprint);
1606
- 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") }));
1607
1712
  }
1608
1713
  if (who.via === "cookie" && !consumeApprovalNonce(body.get("nonce") ?? "", who.name, nonceKey, body.get("fingerprint") ?? ""))
1609
1714
  return refuse(response, who, 409, "This instruction preview expired. Review it again.", "/control");
@@ -1647,7 +1752,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1647
1752
  savePrimary(dir, "slack");
1648
1753
  return redirect(response, "/settings/slack");
1649
1754
  }
1650
- 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)
1651
1757
  return show("Enter your Toolroll password to change Slack access.", 403);
1652
1758
  const generation = store.accountOf(who.name).generation;
1653
1759
  if (action === "connect") {
@@ -1750,6 +1856,11 @@ ${postForm("/settings/chat-approval/save", html `\
1750
1856
  return show("Submit one value for each field.");
1751
1857
  if (botId === null)
1752
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
+ }
1753
1864
  const pairingIdentity = authenticateAccount(store, who.name, body.get("password") ?? "");
1754
1865
  if (!pairingIdentity.ok || pairingIdentity.role !== "approver" || pairingIdentity.generation !== who.session.generation)
1755
1866
  return show("Enter your Toolroll password to change your phone pairing.", 403);
@@ -1780,7 +1891,7 @@ ${postForm("/settings/chat-approval/save", html `\
1780
1891
  savePrimary(dir, "teams");
1781
1892
  return redirect(response, "/settings/teams");
1782
1893
  }
1783
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1894
+ if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
1784
1895
  return show("Enter your Toolroll password to change Teams access.", 403);
1785
1896
  const generation = store.accountOf(who.name).generation;
1786
1897
  if (action === "connect") {
@@ -1837,7 +1948,7 @@ ${postForm("/settings/chat-approval/save", html `\
1837
1948
  savePrimary(dir, "discord");
1838
1949
  return redirect(response, "/settings/discord");
1839
1950
  }
1840
- if (!authenticateApprover(who, body.get("password") ?? "").ok)
1951
+ if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
1841
1952
  return show("Enter your Toolroll password to change Discord access.", 403);
1842
1953
  const generation = store.accountOf(who.name).generation;
1843
1954
  if (action === "connect") {
@@ -1976,6 +2087,8 @@ ${postForm("/settings/chat-approval/save", html `\
1976
2087
  const provider = body.get("provider") ?? "";
1977
2088
  if (!isProviderId(provider))
1978
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");
1979
2092
  if (op === "clear") {
1980
2093
  const cleared = clearProviderKey(provider, providerHome);
1981
2094
  const clearedMode = readAuthMode(provider, providerHome);
@@ -2159,7 +2272,8 @@ ${modeWords(terms)
2159
2272
  return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
2160
2273
  }
2161
2274
  const token = body.get("token") ?? "";
2162
- 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) {
2163
2277
  return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
2164
2278
  }
2165
2279
  store.signMode({
@@ -2345,6 +2459,8 @@ ${modeWords(terms)
2345
2459
  "settings.backups": backupsPage,
2346
2460
  "settings.data": dataPage,
2347
2461
  "settings.sign-in": signInPage,
2462
+ "settings.passwordless": passwordlessPage,
2463
+ "settings.passwordless-send": passwordlessSave,
2348
2464
  "settings.lead": leadPage,
2349
2465
  "settings.teams": teamsPage,
2350
2466
  "settings.discord": discordPage,
@@ -2358,12 +2474,15 @@ ${modeWords(terms)
2358
2474
  "settings.page": settingsHome,
2359
2475
  "provider.resume": providerResume,
2360
2476
  "settings.updates-dismiss": updatesDismiss,
2477
+ "session.forget-password": forgetPassword,
2361
2478
  "settings.updates-checks": updatesChecks,
2362
2479
  "settings.skills-revise": skillsRevise,
2363
2480
  "settings.skills-import": skillsAct("import"),
2364
2481
  "settings.skills-change": skillsAct("change"),
2365
2482
  "spend.budget": spendBudget,
2366
2483
  "settings.project-concurrency": projectConcurrency,
2484
+ "settings.project-live-view": projectLiveView,
2485
+ "settings.project-network": projectNetwork,
2367
2486
  "settings.project-delete": projectDelete,
2368
2487
  "settings.policy-send": policySave,
2369
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
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A lead sweep's table (docs/plans/lead-fan-out.md): every child with its state, its verdict and its result, read from
3
+ * facts on each look (the page follows the console's live beat). The only sweep-wide act is "cancel the rest"; steer,
4
+ * cancel or retry one child is that task's own page.
5
+ */
6
+ import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
7
+ import { html, postForm } from "../html.js";
8
+ import { cancelSweepRest, sweepLine, sweepOpen, sweepOpenDigest, sweepTable } from "../lead-sweep.js";
9
+ import { refuse, screen } from "./chrome.js";
10
+ import { handlersOf } from "./handler-registry.js";
11
+ import { redirect } from "./http.js";
12
+ const SWEEP_PATH = /^\/sweeps\/([1-9][0-9]{0,9})(?:\/cancel-rest)?$/;
13
+ const STATE_WORDS = {
14
+ working: "Working", checking: "Checking", "needs-decision": "Needs you", "ready-to-check": "Ready", complete: "Complete", cancelled: "Cancelled", missing: "Gone",
15
+ };
16
+ export function createSweepHandlers(runtime) {
17
+ const { store, clock, sendScreen, chromeFor, evidenceRoot, consoleProjects, liveRefreshSeconds } = runtime;
18
+ const readable = () => runtime.workAccess().repos ?? consoleProjects();
19
+ async function page(ctx) {
20
+ const { url, who, response } = ctx;
21
+ const id = Number(SWEEP_PATH.exec(url.pathname)?.[1]);
22
+ const table = sweepTable(store, id, clock(), readable(), evidenceRoot);
23
+ if (table === null)
24
+ return refuse(response, who, 404, "There's no sweep at this address you can see.", "/chat");
25
+ const said = url.searchParams.get("said");
26
+ const rows = table.rows.map(row => html `<tr data-sweep-row="${row.ordinal}">
27
+ <td>${row.ordinal + 1}</td>
28
+ <td><a href="/t/${encodeURIComponent(row.task)}">${row.label}</a></td>
29
+ <td><span class="chip">${STATE_WORDS[row.state]}</span></td>
30
+ <td>${row.verdict === null ? html `<span class="meta">—</span>` : html `<strong>${row.verdict.verdict}</strong> <span class="meta">${row.verdict.why}</span>`}</td>
31
+ <td>${row.result === null ? "" : html `<a href="/t/${encodeURIComponent(row.result.taskId)}">Result →</a>`}</td>
32
+ </tr>`);
33
+ // The form names exactly what it would act on (each item's active task, state and attempt): a change is refused.
34
+ const open = sweepOpen(store, table, clock());
35
+ const running = open.filter(one => one.run !== null).length;
36
+ const cancel = who.role !== "approver" || open.length === 0 ? "" : html `<details class="card"><summary>Cancel the rest (${open.length})</summary>${postForm(`/sweeps/${id}/cancel-rest`, html `<p>Cancels ${open.length - running} unfinished item${open.length - running === 1 ? "" : "s"}${running === 0 ? "" : ` and stops ${running} running build${running === 1 ? "" : "s"}`}: ${open.map(one => one.label).join(", ")}. Finished results stay.</p><label class="arm"><input type="checkbox" name="confirm" value="yes" required> Cancel the rest</label><button class="danger">Cancel the rest</button>`, { hidden: { snapshot: sweepOpenDigest(open) } })}</details>`;
37
+ const body = html `<h1>${table.title}</h1>${said === null ? "" : html `<p role="status">${said}</p>`}
38
+ <p class="meta">${sweepLine(table)} · at most ${table.maxParallel} at once · each item is its own task in its own worktree</p>
39
+ <table class="sweep-table"><thead><tr><th>#</th><th>Item</th><th>State</th><th>Verdict</th><th></th></tr></thead><tbody>${rows}</tbody></table>${cancel}`;
40
+ return sendScreen(response, 200, screen(table.title, body, { chrome: chromeFor(table.repo, "work"), refreshSeconds: liveRefreshSeconds() }));
41
+ }
42
+ async function cancelRest(ctx) {
43
+ const { url, who, response, posted } = ctx;
44
+ const id = Number(SWEEP_PATH.exec(url.pathname)?.[1]);
45
+ const body = readForm(posted, CONSOLE_FORMS.sweep);
46
+ const table = sweepTable(store, id, clock(), readable(), evidenceRoot);
47
+ if (table === null)
48
+ return refuse(response, who, 404, "There's no sweep at this address you can see.", "/chat");
49
+ if (body.get("confirm") !== "yes")
50
+ return redirect(response, `/sweeps/${id}?said=${encodeURIComponent("Tick the box to cancel the rest.")}`);
51
+ const now = clock();
52
+ const open = sweepOpen(store, table, now);
53
+ if (body.get("snapshot") !== sweepOpenDigest(open))
54
+ return redirect(response, `/sweeps/${id}?said=${encodeURIComponent("The sweep changed while you were reading. Look again, then cancel.")}`);
55
+ return redirect(response, `/sweeps/${id}?said=${encodeURIComponent(cancelSweepRest(store, who.name, open, now, "web"))}`);
56
+ }
57
+ return { registrations: handlersOf("tasks", { "sweep.page": page, "sweep.cancel-rest": cancelRest }) };
58
+ }
@@ -11,6 +11,7 @@ import { type ProjectPeek, type WorkRow } from "./render-pages.js";
11
11
  import { type CompletionReceiptView, type ResultDetail, type TaskPullRequest } from "./render-results.js";
12
12
  import { taskBody, type MilestoneProgressView, type PlanContractView, type PlanRevisionLedgerView, type RevisionDocView, type RevisionView, type RouteView } from "./render-tasks.js";
13
13
  import { type Who } from "./session.js";
14
+ import type { TaskPageExtras } from "./runtime.js";
14
15
  import type { PlanRevision, TaskRef } from "../store.js";
15
16
  import { type Run, type Store, type Task, type TaskState } from "../store.js";
16
17
  import { type ReviewFacts } from "../workspace-ui.js";
@@ -74,7 +75,7 @@ export declare function createTaskViews(runtime: TaskViewsRuntime): {
74
75
  level: "quick" | "full";
75
76
  returnTo: string;
76
77
  } | null;
77
- taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean) => void;
78
+ taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, extras?: TaskPageExtras) => void;
78
79
  armTaskResume: (response: ServerResponse, who: Who, taskId: string, named: string, returnTo: "chat" | "task", now: Date) => void;
79
80
  planViewOf: (taskRef: number) => {
80
81
  document: string;
@@ -1,6 +1,9 @@
1
+ import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
2
+ import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
3
+ import { originOf } from "../task-source.js";
1
4
  import { taskBaseOf } from "../task-base.js";
2
5
  import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
3
- import { html, joinHtml } from "../html.js";
6
+ import { html, htmlString, joinHtml } from "../html.js";
4
7
  import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
5
8
  import { assignmentOf } from '../assignment.js';
6
9
  import { knowledgeContextHtml } from "../knowledge-ui.js";
@@ -18,7 +21,7 @@ import { taskFingerprint } from "../task-live.js";
18
21
  import { pullRequestFactOf } from '../task-status.js';
19
22
  import { openWorkDecisionOf } from "../work-summary.js";
20
23
  import { learningHtml } from "../workspace-ui.js";
21
- import { randomBytes } from "node:crypto";
24
+ import { createHash, randomBytes } from "node:crypto";
22
25
  import {} from "node:http";
23
26
  import { agentChoicesFor, routeOfTask } from "../agentconfig.js";
24
27
  import { rulesSummary } from "../approval-rules-ui.js";
@@ -41,6 +44,8 @@ import {} from "./render-pages.js";
41
44
  import { completionReceiptView, diffFileAnchor, evidenceLinksFor, followUpsFor, orderChangedFiles, parseReviewDiff, proofBundleView, receiptStatusOf, withinSignedTouches } from "./render-results.js";
42
45
  import { approvalFormDigest, consentDoorOf, nonceHashOf, publicationFactsOf, resumeCeremonyPage, resumeDigestOf, taskBody, taskPage } from "./render-tasks.js";
43
46
  import {} from "./session.js";
47
+ import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
48
+ import { reviewEvidenceScript } from "./render-results.js";
44
49
  import { budgetHoldWords, budgetLabel, monthOf } from "../spend.js";
45
50
  import {} from "../store.js";
46
51
  import { taskControlOf } from "../task-control.js";
@@ -367,6 +372,8 @@ export function createTaskViews(runtime) {
367
372
  repairChain: completion !== null ? null : runtime.store.repairChainForDraft(taskId),
368
373
  repo: ref?.repo ?? null,
369
374
  filedVia: runtime.store.filedViaOf(taskId),
375
+ // v126: where the work came from (a flow, an outside event), from what was kept when it was filed.
376
+ origin: (() => { const repos = runtime.workAccess().repos; return originOf(runtime.store, taskId, repo => repos === null || repos.includes(repo)); })(),
370
377
  coordinator: (() => {
371
378
  const who = runtime.store.coordinatorProvenanceOf(taskId);
372
379
  if (who === null)
@@ -485,6 +492,31 @@ export function createTaskViews(runtime) {
485
492
  steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
486
493
  base: taskBaseOf(runtime.store, taskId),
487
494
  steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
495
+ steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
496
+ approvalProof: (() => {
497
+ // The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
498
+ const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
499
+ return scope === null || scope.approvedBy === null || scope.approvedDigest === null ? null
500
+ : approvalProofOf(runtime.store, ref.externalId, scope.approvedBy, scope.approvedDigest);
501
+ })(),
502
+ // Cockpit B2: whether this person's remembered password may approve these exact terms (the same rule approve()
503
+ // applies), and since when.
504
+ approvalFresh: (() => {
505
+ const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
506
+ if (scope === null || who?.via !== "cookie")
507
+ return null;
508
+ const fresh = withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true));
509
+ if (fresh === null)
510
+ return null;
511
+ const gate = runtime.store.approvalGate(ref.externalId, who.name, "person");
512
+ return freshlyApprovable(runtime.store, ref.externalId, scope, gate.verdict === "seal" && !gate.protectedWork) ? new Date(fresh.at).toISOString() : null;
513
+ })(),
514
+ steerRecent: ref === null ? [] : runtime.store.runsFor(ref.id).filter(one => one.outcome !== null).map(one => one.id).sort((a, b) => b - a).slice(0, 5),
515
+ // The running attempt's composer, for an approver on a browser session (steer-controls.ts).
516
+ steerComposer: (() => {
517
+ const run = ref === null ? null : liveAttemptOf(runtime.store, ref.id, now);
518
+ return run === null ? null : steerControlsFor(runtime.store, ref.externalId, run, now, who?.role === "approver");
519
+ })(),
488
520
  // "publishes as" reads ONLY publicationGrantFor(repo) — the grant
489
521
  // the publisher would act under — never listGrants(), which is
490
522
  // dispatch authority (slice 1c).
@@ -584,6 +616,7 @@ export function createTaskViews(runtime) {
584
616
  deliverable: view.deliverable ?? "branch",
585
617
  revision: view.revision ?? null,
586
618
  coordinator: view.coordinator ?? null,
619
+ origin: view.origin ?? null,
587
620
  repairChain: view.repairChain ?? null,
588
621
  },
589
622
  decisions: (view?.decisions ?? runtime.store.decisionsForTask(ref.id))
@@ -690,7 +723,53 @@ export function createTaskViews(runtime) {
690
723
  return null;
691
724
  return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
692
725
  }
693
- function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
726
+ /**
727
+ * One page per task (docs/plans/one-page-task-and-agent.md): the tab the address named, the Result tab's result with
728
+ * every result attempt to pick from, and every link that would open a result page of this family (its /review deep
729
+ * link, a build's /r/<n>) pointed at this page's Result tab instead — the person stays on the task.
730
+ */
731
+ function onePageView(view, family, runs, extras) {
732
+ const root = family?.root.id ?? view.id;
733
+ const current = family?.current.id ?? view.id;
734
+ const versions = new Set(family?.versions.map(one => one.id) ?? [view.id]);
735
+ const results = new Map(runs.filter(run => runIsTaskResult(run) && (extras.admissible === undefined || extras.admissible.has(run.id))).map(run => [run.id, run.taskId]));
736
+ const tabHref = (taskId, run, section, hash) => `${taskHref(root)}?${taskId !== current ? `version=${encodeURIComponent(taskId)}&` : ""}tab=result${run === null ? "" : `&run=${run}`}${section === null || section === "summary" ? "" : `&section=${section}`}${hash}`;
737
+ const toTab = (href) => {
738
+ const review = /^\/review\?result=([A-Za-z0-9._~%-]{1,200})(?:&run=([0-9]{1,15}))?(?:&tab=(summary|changes|checks))?(#[A-Za-z0-9_-]{1,120})?$/.exec(href);
739
+ if (review !== null) {
740
+ const taskId = decodeURIComponent(review[1]);
741
+ if (versions.has(taskId))
742
+ return tabHref(taskId, review[2] === undefined ? null : Number(review[2]), review[3] ?? null, review[4] ?? "");
743
+ }
744
+ const record = /^\/r\/([0-9]{1,15})$/.exec(href);
745
+ if (record !== null && results.has(Number(record[1])))
746
+ return tabHref(results.get(Number(record[1])), Number(record[1]), null, "");
747
+ return href;
748
+ };
749
+ const link = (one) => one === null ? null : { ...one, href: one.href === null ? null : toTab(one.href) };
750
+ const status = view.status === null ? null : { ...view.status, action: view.status.action === null ? null : link(view.status.action),
751
+ status: { ...view.status.status, details: view.status.status.details.map(one => ({ ...one, href: one.href === null ? null : toTab(one.href), action: one.action === null ? null : link(one.action) })) } };
752
+ const selected = extras.result?.selected ?? null;
753
+ const attempts = [...results.entries()].sort((a, b) => b[0] - a[0]).map(([id, taskId]) => {
754
+ const run = runs.find(one => one.id === id);
755
+ const outcome = run.outcome === "built" ? "result" : run.outcome === "no-change" ? "no change" : run.outcome === null ? "never finished" : run.outcome;
756
+ return { label: `${run.role === "scout" ? "Report" : "Build"} #${id} · ${outcome}`, href: tabHref(taskId, id, null, ""), current: selected?.build === id };
757
+ });
758
+ return {
759
+ ...view,
760
+ status,
761
+ ...(view.failure == null ? {} : { failure: { ...view.failure, link: link(view.failure.link) } }),
762
+ ...(view.earlier == null ? {} : { earlier: { ...view.earlier, attempts: view.earlier.attempts.map(one => ({ ...one, href: toTab(one.href) })) } }),
763
+ ...(view.thread === undefined ? {} : { thread: view.thread.map(one => ({ ...one, link: link(one.link) })) }),
764
+ // The previous result's fold moves to the Result tab when there is one.
765
+ sections: selected === null ? view.sections : view.sections.filter(one => one.id !== "result"),
766
+ tab: extras.tab ?? "activity",
767
+ result: selected === null ? null : { selected, current: extras.result.current, attempts,
768
+ // Fresh per-render request tokens (32 hex) are not what it shows: left out, so an unchanged result keeps its stamp.
769
+ revision: createHash("sha256").update(JSON.stringify(selected).replace(/\b[a-f0-9]{32}\b/g, "")).digest("hex").slice(0, 16) },
770
+ };
771
+ }
772
+ function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false, extras = {}) {
694
773
  const admittedFamily = runtime.familyOf(taskId);
695
774
  if (admittedFamily?.problem != null)
696
775
  return runtime.sendScreen(response, status, screen(admittedFamily.root.title, html `<h1>${admittedFamily.root.title}</h1><p>${admittedFamily.current.state}</p><p class="problem" data-history-problem>${admittedFamily.problem}</p>`, { chrome: runtime.chromeFor(admittedFamily.root.repo, "tasks") }));
@@ -738,10 +817,21 @@ export function createTaskViews(runtime) {
738
817
  catch {
739
818
  return null;
740
819
  } })() } } : {}) } : page.workspace?.view;
820
+ // A refusal that keeps a draft (a scope edit, an armed cancel) or asks for the plan editor reopens on Plan, where the
821
+ // draft and its error are; anything else on Activity unless the address named a tab.
822
+ const tab = extras.tab ?? (scopeDraft !== undefined || cancelDraft !== undefined || editPlan ? "plan" : "activity");
823
+ const oneTaskView = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
824
+ // The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
825
+ if (extras.result != null) {
826
+ page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
827
+ // The result's own script, as its result page ships it (a password form there keeps it too: Request changes and
828
+ // the line notes need it).
829
+ page.functional = { script: `${page.functional?.script ?? ""}${reviewEvidenceScript()}${RESULT_REVIEW_SCRIPT}`, fetches: page.functional?.fetches ?? false };
830
+ }
741
831
  if (docked !== null)
742
- page.workspace = { ...page.workspace, ...(taskView === undefined ? {} : { view: taskView }), conversation: docked, pageHtml: page.body };
743
- else if (taskView !== undefined && page.workspace !== undefined)
744
- page.workspace = { ...page.workspace, view: taskView };
832
+ page.workspace = { ...page.workspace, ...(oneTaskView === undefined ? {} : { view: oneTaskView }), conversation: docked, pageHtml: page.body };
833
+ else if (oneTaskView !== undefined && page.workspace !== undefined)
834
+ page.workspace = { ...page.workspace, view: oneTaskView };
745
835
  page.refreshSeconds = runtime.liveRefreshSeconds();
746
836
  return runtime.sendScreen(response, status, page);
747
837
  }