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
package/dist/operate.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { ACP_AGENTS, ACP_PROVIDER_IDS, isAcpProvider, isKeyedProvider } from "./acp-agents.js";
2
+ import { inspectAcpAgent } from "./acp-inspect.js";
3
+ import { runAcpChecks } from "./acp-check.js";
4
+ import { verifyLaunch } from "./acp-attest.js";
1
5
  import { notificationClaimHeld, claimNotifications, finalizeNotification, notificationDestination } from "./notification-delivery.js";
2
6
  import { adapterPolicy } from "./server/route-policy.js";
3
7
  import { UNSENT_REPLY_MS } from "./telegram-settings.js";
@@ -52,6 +56,7 @@ import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
52
56
  import { sendSubagentSummaries } from "./subagent-admin.js";
53
57
  import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
54
58
  import { refreshConnections } from "./mcp-connect.js";
59
+ import { SECRETS_FIRST, startToolSecrets, toolSecretStorage } from "./project-tools.js";
55
60
  import { followDiscord } from "./discord.js";
56
61
  import { followTeams } from "./teams.js";
57
62
  import { followSlack } from "./slack.js";
@@ -94,6 +99,7 @@ import { basename, dirname, join, resolve } from "node:path";
94
99
  import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
95
100
  import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
96
101
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
102
+ import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
97
103
  import { authorizePlanUnderMode } from "./plan-auto.js";
98
104
  import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
99
105
  import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
@@ -104,6 +110,7 @@ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync,
104
110
  import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
105
111
  import { claimActor, currentActor, withActor } from "./actor.js";
106
112
  import { parseLeadToken } from "./api-tokens.js";
113
+ import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
107
114
  import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
108
115
  import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
109
116
  import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
@@ -181,7 +188,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
181
188
  import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
182
189
  import { serveMcp } from "./mcp.js";
183
190
  import { createInterface } from "node:readline";
184
- import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
191
+ import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin } from "./scope.js";
185
192
  import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
186
193
  import { WorktreePool } from "./worktree.js";
187
194
  import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
@@ -250,6 +257,11 @@ export const OPERATE_HELP = `toolroll — operating the queue
250
257
  toolroll project show show the current project and credential reference
251
258
  toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
252
259
  --not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
260
+ toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
261
+ what the project's builds may reach on the network; an approver changes it
262
+ (new scopes seal it; approved ones keep theirs)
263
+ toolroll config unconfined-agents [allow|refuse]
264
+ whether builds run where this machine can't confine them (instance operator)
253
265
  toolroll project concurrency [<n>] --repo <p>
254
266
  how many of a project's tasks build at once (default 2, never past the
255
267
  worker's capacity); an approver changes it with <n>
@@ -751,7 +763,8 @@ export function parseOperateArgs(argv, ownValues = new Set()) {
751
763
  }
752
764
  /** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
753
765
  export async function runOperate(command, argv, write, options = {}) {
754
- return withPasswordSource(CLI_PASSWORD_SOURCE, () => runOperateLocal(command, argv, write, options));
766
+ // Each command gets its own remembered-login holder (Passwordless: "cli-login" is this command's, never the next one's).
767
+ return withPasswordSource(CLI_PASSWORD_SOURCE, () => rememberedLogin.run({ login: null }, () => runOperateLocal(command, argv, write, options)));
755
768
  }
756
769
  async function runOperateLocal(command, argv, write, options = {}) {
757
770
  const parsed = parseOperateArgs(argv, command === "flows" ? FLOWS_VALUE_FLAGS : undefined);
@@ -815,6 +828,9 @@ async function runOperateLocal(command, argv, write, options = {}) {
815
828
  catch (error) {
816
829
  return databaseFailure(write, json, command, file, error);
817
830
  }
831
+ // Toolroll's folder for the secrets it holds, beside this database: made before any build can run.
832
+ if (file !== ":memory:")
833
+ startToolSecrets(store, file, () => new Date(), { eager: SECRETS_FIRST.has(command) });
818
834
  // The lead (a lead token, as --token or TOOLROLL_LEAD_TOKEN) acts for its owner in single commands; its work
819
835
  // pings nobody. Anyone else who signs in inside one of these commands is who acted. Services name nobody.
820
836
  const presentedFlag = text(flags, "token");
@@ -1141,6 +1157,8 @@ async function dispatch(command, positional, flags, context) {
1141
1157
  return projectDeleteCommand(positional, flags, context);
1142
1158
  if (positional[0] === "concurrency")
1143
1159
  return projectConcurrencyCommand(positional, flags, context);
1160
+ if (positional[0] === "network")
1161
+ return projectNetworkCommand(positional, flags, context);
1144
1162
  if (positional[0] === "checks")
1145
1163
  return projectChecksCommand(positional, flags, context);
1146
1164
  if (positional[0] === "demo")
@@ -1219,6 +1237,8 @@ async function dispatch(command, positional, flags, context) {
1219
1237
  case "flows":
1220
1238
  return flowsCommand(positional, flags, context);
1221
1239
  case "config":
1240
+ if (positional[0] === "unconfined-agents")
1241
+ return unconfinedAgentsCommand(positional, flags, context);
1222
1242
  return configCommand(positional, flags, context);
1223
1243
  case "chat":
1224
1244
  return chatCommand(flags, context);
@@ -1239,6 +1259,8 @@ async function dispatch(command, positional, flags, context) {
1239
1259
  });
1240
1260
  case "keys":
1241
1261
  return keysCommand(positional, flags, context);
1262
+ case "agents":
1263
+ return agentsCommand(positional, flags, context);
1242
1264
  case "setup":
1243
1265
  return setupCommand(positional, flags, context);
1244
1266
  case "verify":
@@ -1484,6 +1506,9 @@ function claimCommand(positional, flags, context) {
1484
1506
  if (result.reason === "coordinator-filed") {
1485
1507
  return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
1486
1508
  }
1509
+ if (result.reason === "sweep-limit") {
1510
+ return fail(write, json, "claim", "sweep-limit", `its sweep already has ${result.limit} running at once — it starts when one finishes`, EXIT.refused, { sweep: result.sweep, limit: result.limit });
1511
+ }
1487
1512
  return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
1488
1513
  }
1489
1514
  // Taking a task is what makes it running; leaving that to the caller would
@@ -1602,7 +1627,7 @@ async function runnerCommand(positional, flags, context) {
1602
1627
  if (acting === null) {
1603
1628
  return fail(write, json, "runner register", "usage", "`runner register <name> --repo <path> --as <you> --token <t>` — minting runner authority is an operator act", EXIT.usage);
1604
1629
  }
1605
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1630
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1606
1631
  if (!authenticated.ok) {
1607
1632
  return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1608
1633
  }
@@ -1728,7 +1753,7 @@ async function runnerCommand(positional, flags, context) {
1728
1753
  if (acting === null) {
1729
1754
  return fail(write, json, "runner bind", "usage", "`runner bind <name> --repo <path> --as <you> --token <t>` — binding repositories is an operator act", EXIT.usage);
1730
1755
  }
1731
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1756
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1732
1757
  if (!authenticated.ok) {
1733
1758
  return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1734
1759
  }
@@ -1814,7 +1839,7 @@ async function coordinatorCommand(positional, flags, context) {
1814
1839
  if (acting === null) {
1815
1840
  return fail(write, json, "coordinator mint", "usage", "`coordinator mint <name> --repo <path> --as <you> --token <t>` — minting filing authority is an operator act", EXIT.usage);
1816
1841
  }
1817
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1842
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1818
1843
  if (!authenticated.ok) {
1819
1844
  return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1820
1845
  }
@@ -1940,6 +1965,7 @@ async function mcpCommand(file, flags, write, json) {
1940
1965
  if (!door.ok)
1941
1966
  return said(door.message);
1942
1967
  const store = door.store;
1968
+ startToolSecrets(store, file, () => new Date(), { eager: false });
1943
1969
  for (const suffix of ["", "-wal", "-shm"]) {
1944
1970
  try {
1945
1971
  chmodSync(`${file}${suffix}`, 0o600);
@@ -2287,6 +2313,17 @@ async function tickCommand(flags, context) {
2287
2313
  root: pool,
2288
2314
  ...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
2289
2315
  });
2316
+ // ACP agent checks (acp-check.ts) run in a watch's own pass, under its lease, before anything is dispatched — one
2317
+ // at a time per runner. Only a watch's full pass runs them (never a build lane, never a bare tick).
2318
+ const watchOwner = text(flags, "incarnation");
2319
+ if (watchOwner !== undefined && !buildsOnly) {
2320
+ await runAcpChecks({
2321
+ store, runner, token, pool: worktrees, clock,
2322
+ watch: { repo: canonicalProject(repo) ?? resolve(repo), owner: watchOwner },
2323
+ progress: line => { if (!json)
2324
+ write(line); },
2325
+ });
2326
+ }
2290
2327
  // Where a first attempt's branch grows from, resolved once per pass. A
2291
2328
  // detached HEAD refuses the pass rather than guessing: an unattended commit
2292
2329
  // onto "wherever the operator happened to be" is not a default.
@@ -2611,6 +2648,20 @@ async function tickCommand(flags, context) {
2611
2648
  : [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
2612
2649
  let unattestedLane = null;
2613
2650
  for (const candidate of new Set(skipProviders)) {
2651
+ // An ACP agent is admitted by its own check on this runner (absence is denial), read here the way the gateway reads it.
2652
+ if (isAcpProvider(candidate)) {
2653
+ const checked = store.acpAttestationFor(candidate, runner);
2654
+ const verified = checked === null ? null : verifyLaunch(checked.launch);
2655
+ if (checked === null) {
2656
+ unattestedLane = `${ACP_AGENTS[candidate].name} isn't checked on ${runner} — run \`toolroll agents check ${candidate}\` there`;
2657
+ break;
2658
+ }
2659
+ if (verified !== null && !verified.ok) {
2660
+ unattestedLane = verified.problem;
2661
+ break;
2662
+ }
2663
+ continue;
2664
+ }
2614
2665
  if (attestationOf(candidate) === null)
2615
2666
  continue;
2616
2667
  let verdict = attestedThisPass.get(candidate);
@@ -2641,7 +2692,9 @@ async function tickCommand(flags, context) {
2641
2692
  // budget is raised (the 100% alert went out; the task page and the spend page say why).
2642
2693
  // Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
2643
2694
  // Each provider as it bills now.
2644
- const budgeted = budgetHold(ref.id, store.agentsFor([spec.provider, ...skipProviders]));
2695
+ // An ACP agent bills as its declaration for the exact model it would run on this runner.
2696
+ const sealedModel = store.getScope(id)?.approvedProfile?.model ?? null;
2697
+ const budgeted = budgetHold(ref.id, [...new Set([spec.provider, ...skipProviders])].map(provider => isAcpProvider(provider) ? store.agentFor(provider, provider === spec.provider ? spec.model ?? sealedModel : sealedModel, runner) : store.agentsFor([provider])[0]));
2645
2698
  if (budgeted.over !== null) {
2646
2699
  dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
2647
2700
  continue;
@@ -3895,6 +3948,7 @@ async function startConsole(options) {
3895
3948
  ...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
3896
3949
  ...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
3897
3950
  ...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
3951
+ ...(options.instanceSecret === undefined ? {} : { instanceSecret: options.instanceSecret }),
3898
3952
  });
3899
3953
  await new Promise((resolve, reject) => {
3900
3954
  server.once("error", reject);
@@ -4030,6 +4084,8 @@ async function requeueTask(positional, flags, context) {
4030
4084
  claimed: `a runner holds ${id} right now — wait for the attempt to end, or stop it`,
4031
4085
  "accepted-result": `${id}'s last result was accepted; change it through a revision, not a rerun`,
4032
4086
  published: `${id}'s last result is published; revise it through its pull request`,
4087
+ // Only an Approve & build again asks for an exact build; the CLI never does.
4088
+ moved: `${id} changed since its page was read; read it again`,
4033
4089
  };
4034
4090
  return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
4035
4091
  }
@@ -4312,6 +4368,13 @@ async function providersCommand(flags, context) {
4312
4368
  // that serializes four of them reads as a hang (Phase 3).
4313
4369
  const probed = await Promise.all(PROVIDER_IDS.map(async (id) => {
4314
4370
  const facts = inspectionOf(id);
4371
+ // An ACP agent is looked at, never run (acp-inspect.ts): its version is its package's, read; whether it may
4372
+ // run is its conformance check.
4373
+ if (isAcpProvider(id)) {
4374
+ const seen = inspectAcpAgent(id);
4375
+ const version = { code: seen.found ? 0 : 127, stdout: seen.found ? (seen.package?.version ?? "") : "", stderr: "", notFound: !seen.found, timedOut: false };
4376
+ return { id, facts, version, installed: seen.found, identity: seen.found ? `not checked on this computer — run \`toolroll agents check ${id}\`` : null };
4377
+ }
4315
4378
  // A version probe carries no key (Codex gemini verify, finding 2).
4316
4379
  const version = await probe(facts.binary, ["--version"], { timeoutMs: 5_000, omitEnv: ALL_CREDENTIAL_ENV });
4317
4380
  const installed = version.code === 0 && !version.notFound;
@@ -4368,7 +4431,7 @@ async function providersCommand(flags, context) {
4368
4431
  if (runnerName === undefined || runnerToken === undefined) {
4369
4432
  return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
4370
4433
  }
4371
- const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }));
4434
+ const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }), process.env, PROVIDER_IDS, provider => store.acpAttestationFor(provider, runnerName));
4372
4435
  const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
4373
4436
  if (!reported.ok) {
4374
4437
  return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
@@ -4439,6 +4502,83 @@ async function providersCommand(flags, context) {
4439
4502
  * own — these are 0600 files under the operator's home, and whoever can
4440
4503
  * write them already owns the machine's spend.
4441
4504
  */
4505
+ /**
4506
+ * `toolroll agents …` — the coding agents Toolroll runs over ACP (acp-agents.ts): what is installed here, which are
4507
+ * checked on which runner, and how each checked model is billed. An agent runs only once its conformance check passed
4508
+ * on that runner (acp-check.ts); asking for one, or declaring a model billed by the agent's own plan, takes a password.
4509
+ *
4510
+ * agents each agent: installed, checked, billing
4511
+ * agents check <agent> --model <m> [--runner r] ask the runner's watch to check it (password)
4512
+ * agents revoke <agent> [--runner r] the agent stops running there
4513
+ * agents billing <agent> <model> plan|metered [--runner r]
4514
+ */
4515
+ async function agentsCommand(positional, flags, context) {
4516
+ const { store, write, json, clock } = context;
4517
+ const [action = "list", provider, ...rest] = positional;
4518
+ const runners = store.listRunners().filter(one => one.retiredAt === null).map(one => one.name);
4519
+ const runnerGiven = text(flags, "runner");
4520
+ const runner = runnerGiven ?? (runners.length === 1 ? runners[0] : undefined);
4521
+ if (action === "list") {
4522
+ const rows = ACP_PROVIDER_IDS.map(id => {
4523
+ const seen = inspectAcpAgent(id);
4524
+ const checked = runners.flatMap(name => { const one = store.acpAttestationFor(id, name); return one === null ? [] : [{ runner: name, agent: one.agent, models: one.models, at: one.recordedAt }]; });
4525
+ const latest = store.acpChecksFor(id, 1)[0] ?? null;
4526
+ return { provider: id, name: ACP_AGENTS[id].name, installed: seen.found ? seen.realpath : null, checked, latestCheck: latest === null ? null : { id: latest.id, state: latest.state, model: latest.model, result: latest.result } };
4527
+ });
4528
+ if (json) {
4529
+ write(envelopeJson({ ok: true, command: "agents", agents: rows }));
4530
+ return EXIT.ok;
4531
+ }
4532
+ for (const row of rows) {
4533
+ write(` ${row.name.padEnd(10)} ${row.installed === null ? "not installed" : row.installed}`);
4534
+ for (const one of row.checked)
4535
+ write(` checked on ${one.runner}: ${one.agent.name} ${one.agent.version}, ${one.models.length} models (${one.at.slice(0, 10)})`);
4536
+ if (row.latestCheck !== null && row.latestCheck.state !== "passed")
4537
+ write(` check #${row.latestCheck.id} (${row.latestCheck.model}): ${row.latestCheck.state}${row.latestCheck.result === null ? "" : ` — ${row.latestCheck.result}`}`);
4538
+ }
4539
+ write(" → `agents check <agent> --model <id>` asks this runner's watch to check one; it runs on the watch's next pass");
4540
+ return EXIT.ok;
4541
+ }
4542
+ if (!["check", "revoke", "billing"].includes(action))
4543
+ return fail(write, json, "agents", "usage", "`toolroll agents [check|revoke|billing]`", EXIT.usage);
4544
+ if (provider === undefined || !isAcpProvider(provider))
4545
+ return fail(write, json, `agents ${action}`, "usage", `which agent? ${ACP_PROVIDER_IDS.join(", ")}`, EXIT.usage);
4546
+ if (runner === undefined || !runners.includes(runner))
4547
+ return fail(write, json, `agents ${action}`, "usage", `which runner? --runner ${runners.join(" | ") || "<name>"}`, EXIT.usage);
4548
+ const acting = await askCredentials(flags, context);
4549
+ if (acting === null)
4550
+ return fail(write, json, `agents ${action}`, "usage", "this takes your name and password — `--as <you>` and the hidden prompt", EXIT.usage);
4551
+ const verified = verifyApproverByPassword(store, acting.name, acting.token, store.knownRepos().filter(repo => store.accountCanAccess(acting.name, repo)));
4552
+ if (!verified.ok)
4553
+ return fail(write, json, `agents ${action}`, "unauthenticated", "that is not an approver, or the password does not match", EXIT.refused);
4554
+ // Which agents a runner admits, and how they bill, hold for every project the runner serves: an operator's call.
4555
+ if (!store.isInstanceOperator(verified.who.name))
4556
+ return fail(write, json, `agents ${action}`, "forbidden", "only an installation operator can check, revoke or set billing for a runner's agents", EXIT.refused);
4557
+ const name = ACP_AGENTS[provider].name;
4558
+ const now = clock();
4559
+ if (action === "revoke") {
4560
+ const revoked = store.revokeAcpAttestation(provider, runner, verified.who.name, now);
4561
+ return succeed(write, json, "agents revoke", { provider, runner, revoked }, () => [revoked ? `${name} no longer runs on ${runner}; check it again to bring it back.` : `${name} wasn't checked on ${runner}.`]);
4562
+ }
4563
+ if (action === "billing") {
4564
+ const [model, kind] = rest;
4565
+ if (model === undefined || !validModelId(model) || (kind !== "plan" && kind !== "metered"))
4566
+ return fail(write, json, "agents billing", "usage", "`agents billing <agent> <model> plan|metered`", EXIT.usage);
4567
+ const revision = store.declareAcpBilling({ provider, runner, model, kind, by: verified.who.name }, now);
4568
+ return succeed(write, json, "agents billing", { provider, runner, model, kind, revision }, () => [kind === "plan"
4569
+ ? `${name} on ${model} (${runner}) is billed by ${name}'s own plan: its runs count as $0, unmeasured, and monthly budgets don't hold them. Runs already started keep the terms they started with.`
4570
+ : `${name} on ${model} (${runner}) is metered: its cost is unknown, so capped work and hard budgets hold it.`]);
4571
+ }
4572
+ // check
4573
+ const model = text(flags, "model");
4574
+ if (model === undefined || !validModelId(model))
4575
+ return fail(write, json, "agents check", "usage", "`agents check <agent> --model <id>` — a model the agent offers (its own list shows after the check)", EXIT.usage);
4576
+ const id = store.requestAcpCheck({ provider, runner, model, by: verified.who.name }, now);
4577
+ return succeed(write, json, "agents check", { provider, runner, model, request: id }, () => [
4578
+ `Check #${id} asked for: ${name} on ${model}, on ${runner}. Its watch runs it on its next pass (two short runs, in the fence; the model's own spend).`,
4579
+ "→ `toolroll agents` shows how it went.",
4580
+ ]);
4581
+ }
4442
4582
  async function keysCommand(positional, flags, context) {
4443
4583
  const { write, json } = context;
4444
4584
  const [action, provider, ...rest] = positional;
@@ -4446,7 +4586,8 @@ async function keysCommand(positional, flags, context) {
4446
4586
  return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
4447
4587
  }
4448
4588
  if (action === "status") {
4449
- const rows = PROVIDER_IDS.map(one => ({
4589
+ // ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
4590
+ const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
4450
4591
  provider: one,
4451
4592
  envName: PROVIDER_KEY_ENV[one],
4452
4593
  ...keyStatus(one),
@@ -4470,7 +4611,10 @@ async function keysCommand(positional, flags, context) {
4470
4611
  return EXIT.ok;
4471
4612
  }
4472
4613
  if (provider === undefined || !isProviderId(provider)) {
4473
- return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
4614
+ return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
4615
+ }
4616
+ if (!isKeyedProvider(provider)) {
4617
+ return fail(write, json, `keys ${action}`, "refused", `${ACP_AGENTS[provider].name} signs in its own way (\`${ACP_AGENTS[provider].signIn}\`) — Toolroll keeps no key for it`, EXIT.refused);
4474
4618
  }
4475
4619
  if (action === "clear") {
4476
4620
  const cleared = clearProviderKey(provider);
@@ -4575,7 +4719,8 @@ async function peopleCommand(positional, flags, context) {
4575
4719
  if (acting === null) {
4576
4720
  return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
4577
4721
  }
4578
- const authenticated = authenticateApprover(store, acting.name, acting.token);
4722
+ // Inviting and widening access are credential acts; revoking only narrows.
4723
+ const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
4579
4724
  if (!authenticated.ok) {
4580
4725
  return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
4581
4726
  }
@@ -4642,7 +4787,8 @@ async function chatApprovalCommand(positional, flags, context) {
4642
4787
  const acting = await askCredentials(flags, context);
4643
4788
  if (acting === null)
4644
4789
  return fail(write, json, command, "unauthenticated", "Approving from chat is your own setting: pass --as <you> --token <your password> (or sign in once with toolroll up).", EXIT.refused);
4645
- const authed = authenticateApprover(store, acting.name, acting.token);
4790
+ // Turning approving from chat on (or changing its limits) is a credential act; showing or turning it off is not.
4791
+ const authed = (action === "on" ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
4646
4792
  if (!authed.ok)
4647
4793
  return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
4648
4794
  const given = text(flags, "repo");
@@ -4705,7 +4851,8 @@ async function modeCommand(positional, flags, context) {
4705
4851
  if (acting === null) {
4706
4852
  return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
4707
4853
  }
4708
- const authed = authenticateApprover(store, acting.name, acting.token);
4854
+ // Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
4855
+ const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
4709
4856
  if (!authed.ok) {
4710
4857
  return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
4711
4858
  }
@@ -6811,7 +6958,7 @@ async function resolveUpApprover(context, flags, canPrompt) {
6811
6958
  return { approver: selected, approvers: names, verified, passwordFile, mintedPassword: null, notes };
6812
6959
  }
6813
6960
  /** The platform browser opener — detached, silent, never load-bearing. */
6814
- function openBrowser(url) {
6961
+ export function openBrowser(url) {
6815
6962
  // TOOLROLL_BROWSER_COMMAND names another program to open it with (it gets the address as its one argument).
6816
6963
  const chosen = envValue(process.env, "BROWSER_COMMAND");
6817
6964
  const command = chosen !== undefined && chosen !== "" ? [chosen, url]
@@ -7177,9 +7324,12 @@ async function upCommand(flags, context) {
7177
7324
  // proved set on each request, so its project rail and unified chat move
7178
7325
  // with the live builder instead of freezing at process start.
7179
7326
  const activeRepos = new Set(repos);
7327
+ // Cockpit B1: this start's instance secret, minted once it is listening; the console answers its challenges with it.
7328
+ let instanceSecret = null;
7180
7329
  let console_;
7181
7330
  try {
7182
7331
  console_ = await startConsole({
7332
+ instanceSecret: () => instanceSecret,
7183
7333
  context,
7184
7334
  host: hostFlag,
7185
7335
  port,
@@ -7437,7 +7587,7 @@ async function upCommand(flags, context) {
7437
7587
  const observeReadiness = () => {
7438
7588
  if (context.agentRunner !== undefined)
7439
7589
  return;
7440
- void observeProviderReadiness((file, args, options) => run(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }))
7590
+ void observeProviderReadiness((file, args, options) => run(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }), process.env, PROVIDER_IDS, provider => store.acpAttestationFor(provider, runnerName))
7441
7591
  .then(observed => {
7442
7592
  if (!stopping)
7443
7593
  reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, clock());
@@ -7469,6 +7619,21 @@ async function upCommand(flags, context) {
7469
7619
  login: { account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver, file: approverPlan2.passwordFile },
7470
7620
  });
7471
7621
  recordUpConsoleUrl(dirname(context.databaseFile), url, clock());
7622
+ // Cockpit B1: who this console is, for bare `toolroll` to prove before it attaches: a fresh secret in the database
7623
+ // (behind the agent fence), and a discovery-only socket beside it. A socket that can't open only means attaching
7624
+ // isn't offered; the console runs either way.
7625
+ // The socket is taken FIRST: only the console that owns it publishes a record. A second console on the same database
7626
+ // (another port) never replaces the first one's record; it runs, but isn't attached to.
7627
+ let mintedSecret = null;
7628
+ const instanceSocket = await openInstanceSocket(context.databaseFile, { pid: process.pid, build: packageVersion(), database: context.databaseFile, port: console_.port }, () => mintedSecret);
7629
+ if (instanceSocket.ok) {
7630
+ mintedSecret = await claimInstance(store, context.databaseFile, { pid: process.pid, port: console_.port, build: packageVersion(), socket: instanceSocket.name }, clock());
7631
+ instanceSecret = mintedSecret;
7632
+ if (mintedSecret === null)
7633
+ progress("attach: another console is running on this database — `toolroll` in a repository opens that one");
7634
+ }
7635
+ else
7636
+ progress(`attach: ${instanceSocket.problem} — \`toolroll\` in a repository won't find this console`);
7472
7637
  if (fatal === null) {
7473
7638
  if (json) {
7474
7639
  write(envelopeJson({
@@ -7536,7 +7701,15 @@ async function upCommand(flags, context) {
7536
7701
  process.removeListener("SIGINT", stop);
7537
7702
  process.removeListener("SIGTERM", stop);
7538
7703
  retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
7539
- await new Promise(done => console_.server.close(() => done()));
7704
+ if (instanceSocket.ok)
7705
+ instanceSocket.close();
7706
+ if (mintedSecret !== null)
7707
+ clearInstance(store, mintedSecret);
7708
+ let closeProblem = null;
7709
+ await new Promise(done => console_.server.close(error => { closeProblem = error ?? null; done(); }));
7710
+ // A live terminal that couldn't be proved stopped (or any other close failure) is said, never "stopped".
7711
+ if (closeProblem !== null && fatal === null)
7712
+ fatal = closeProblem.message;
7540
7713
  if (fatal !== null) {
7541
7714
  // The startup envelope (when json) already went out; the exit code is
7542
7715
  // the health signal (finding 11/20) — never a second envelope.
@@ -7666,7 +7839,7 @@ async function bridgeCommand(positional, flags, context) {
7666
7839
  return fail(write, json, `bridge ${action}`, "usage", "`--as <you> --token <your password>` — pairing hands your authority to a chat, so it takes your credential", EXIT.usage);
7667
7840
  }
7668
7841
  const { name: asWho, token } = acting;
7669
- const authenticated = authenticateApprover(store, asWho, token);
7842
+ const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
7670
7843
  if (!authenticated.ok) {
7671
7844
  return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
7672
7845
  }
@@ -8480,7 +8653,8 @@ async function integrationsCommand(positional, flags, context) {
8480
8653
  const counts = { connected: 0, "not-set-up": 0, broken: 0 };
8481
8654
  for (const one of list)
8482
8655
  counts[one.state] += 1;
8483
- return succeed(context.write, context.json, command, { integrations: list, counts }, () => renderIntegrations(list));
8656
+ const secrets = toolSecretStorage();
8657
+ return succeed(context.write, context.json, command, { integrations: list, counts, secrets: { kind: secrets.kind, words: secrets.words, ...(secrets.problem === undefined ? {} : { problem: secrets.problem }) } }, () => renderIntegrations(list, secrets));
8484
8658
  }
8485
8659
  async function waitTask(positional, flags, context) {
8486
8660
  const command = "task wait";
@@ -9445,7 +9619,7 @@ async function leadCommand(positional, flags, context) {
9445
9619
  if (currentActor()?.lead === true)
9446
9620
  return fail(write, json, command, "refused", "A lead cannot mint lead tokens. The person it acts for runs this with their password.", EXIT.refused);
9447
9621
  const acting = await askCredentials(flags, context);
9448
- const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
9622
+ const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
9449
9623
  if (acting === null || verified === null || !verified.ok) {
9450
9624
  return fail(write, json, command, "unauthenticated", "A lead token is minted behind your password: pass --as and --token (or use the remembered login).", EXIT.refused);
9451
9625
  }
@@ -10359,6 +10533,10 @@ function credentialsFrom(flags, context) {
10359
10533
  if (remembered !== null && (name === undefined || name === remembered.name)) {
10360
10534
  name = remembered.name;
10361
10535
  token = remembered.password;
10536
+ // This command proves with the remembered login: under Passwordless that reads as "cli-login", never as typed.
10537
+ const holder = rememberedLogin.getStore();
10538
+ if (holder !== undefined)
10539
+ holder.login = { name, token };
10362
10540
  }
10363
10541
  }
10364
10542
  return { name, token };
@@ -10379,6 +10557,12 @@ async function askCredentials(flags, context) {
10379
10557
  * which scope.ts's authenticateAccount takes as that person (a viewer for a read token) and never checks as a
10380
10558
  * password; these add the token's own project limit.
10381
10559
  */
10560
+ /** A credential act from the CLI (docs/plans/passwordless.md §5): a password typed for this command; with Passwordless on,
10561
+ * never the remembered login. */
10562
+ function credentialApprover(store, by, token, repo) {
10563
+ const proved = authenticateApprover(store, by, token, repo);
10564
+ return !proved.ok || credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
10565
+ }
10382
10566
  function authenticateApprover(store, by, token, repo) {
10383
10567
  const remote = activeRemote();
10384
10568
  if (remote !== null && repo !== undefined && !remote.allows(repo))
@@ -10728,6 +10912,86 @@ async function projectRulesCommand(positional, flags, context) {
10728
10912
  context.store.setApprovalRules(repo, next, acting.name, context.clock());
10729
10913
  return succeed(context.write, context.json, command, { repo, rules: next }, () => [`${repo}: ${rulesSummary(next)}`]);
10730
10914
  }
10915
+ /**
10916
+ * `project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]`: what the project's
10917
+ * builds may reach (egress.ts). New scopes are written with it and their approvals seal it; scopes already approved
10918
+ * keep the network they were approved with. Widening or narrowing it is an approver's act with a password typed now,
10919
+ * kept in the ledger as before → after.
10920
+ */
10921
+ async function projectNetworkCommand(positional, flags, context) {
10922
+ const command = "project network";
10923
+ const allowed = new Set(["repo", "allow", "remove", "open", "allowlist", "local", "as", "token", "token-file", "token-env", "db", "json"]);
10924
+ for (const name of flags.keys())
10925
+ if (!allowed.has(name))
10926
+ return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
10927
+ const repoFlag = text(flags, "repo");
10928
+ const local = flags.get("local");
10929
+ if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none")) {
10930
+ return fail(context.write, context.json, command, "usage", "Use project network --repo <project path> [--allow host,…] [--remove host,…] [--open | --allowlist] [--local any|none].", EXIT.usage);
10931
+ }
10932
+ const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
10933
+ const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
10934
+ const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
10935
+ if (repo === undefined)
10936
+ return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
10937
+ const current = context.store.projectNetwork(repo);
10938
+ const words = (one) => [
10939
+ `${repo}: ${one.mode === "open" ? "builds may reach any host" : `builds reach their agent's provider, the package registries the project uses${one.hosts.length === 0 ? "" : `, and ${one.hosts.join(", ")}`}`}.`,
10940
+ one.loopback === "any" ? "On a Mac, builds may reach this Mac's local services (never Toolroll's own)." : "On a Mac, builds reach no local services but their own proxy and tools.",
10941
+ ];
10942
+ const listOf = (name) => {
10943
+ const given = text(flags, name);
10944
+ if (given === undefined)
10945
+ return [];
10946
+ const hosts = given.split(",").map(one => one.trim()).filter(Boolean).map(normalizeHost);
10947
+ return hosts.some(one => one === null) ? null : hosts;
10948
+ };
10949
+ const add = listOf("allow"), remove = listOf("remove");
10950
+ if (add === null || remove === null)
10951
+ return fail(context.write, context.json, command, "usage", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path.", EXIT.usage);
10952
+ const changing = add.length > 0 || remove.length > 0 || flags.has("open") || flags.has("allowlist") || local !== undefined;
10953
+ if (!changing)
10954
+ return succeed(context.write, context.json, command, { repo, network: current }, () => words(current));
10955
+ const next = {
10956
+ mode: flags.has("open") ? "open" : flags.has("allowlist") ? "allowlist" : current.mode,
10957
+ hosts: [...new Set([...current.hosts.filter(one => !remove.includes(one)), ...add])].sort(),
10958
+ loopback: local === "any" ? "any" : local === "none" ? "proxy-only" : current.loopback,
10959
+ };
10960
+ if (next.hosts.length > MAX_NETWORK_HOSTS)
10961
+ return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
10962
+ const acting = await askCredentials(flags, context);
10963
+ const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
10964
+ if (acting === null || verified === null || !verified.ok || !context.store.accountCanAccess(acting.name, repo)) {
10965
+ return fail(context.write, context.json, command, "refused", "An approver for this project changes what its builds may reach, with their password: pass --as and --token.", EXIT.refused);
10966
+ }
10967
+ const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
10968
+ return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
10969
+ ...words(changed.after),
10970
+ "Scopes written from now on use this; scopes already approved keep the network they were approved with.",
10971
+ ]);
10972
+ }
10973
+ /** `config unconfined-agents [allow|refuse]`: whether a build may run on a machine that can't confine it at the OS
10974
+ * (no macOS sandbox, no working bubblewrap). Refused unless an instance operator allows it, with a password. */
10975
+ async function unconfinedAgentsCommand(positional, flags, context) {
10976
+ const command = "config unconfined-agents";
10977
+ const { store, write, json } = context;
10978
+ const wanted = positional[1];
10979
+ if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse")) {
10980
+ return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
10981
+ }
10982
+ const words = (allowed) => [allowed
10983
+ ? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
10984
+ : "Builds this machine can't confine at the OS don't start."];
10985
+ if (wanted === undefined)
10986
+ return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
10987
+ const acting = await askCredentials(flags, context);
10988
+ const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
10989
+ if (acting === null || verified === null || !verified.ok || !store.isInstanceOperator(acting.name)) {
10990
+ return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
10991
+ }
10992
+ store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
10993
+ return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
10994
+ }
10731
10995
  /** `project concurrency [<n>] --repo <p>`: how many of a project's tasks build at once. Changing it is an approver's act. */
10732
10996
  async function projectConcurrencyCommand(positional, flags, context) {
10733
10997
  const command = "project concurrency";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Passwordless (docs/plans/passwordless.md): one installation setting. When it is on, signing in is the only
3
+ * password — a step-up is satisfied by the person's live browser session (scope.ts `proveAccountNow`, method
4
+ * "session") or, at this computer's CLI, by its remembered login. Credential acts (the plan's closed list) still
5
+ * take a typed password (`proveCredential`).
6
+ *
7
+ * Like the other policy settings it is an append-only ledger entry; the newest one wins; none means off.
8
+ */
9
+ import type { Store } from "./store.js";
10
+ export declare const PASSWORDLESS_ACTION = "passwordless changed";
11
+ export type PasswordlessSetting = {
12
+ on: boolean;
13
+ setBy: string | null;
14
+ at: string | null;
15
+ };
16
+ /** Read live at every act: a change takes effect on the next one. */
17
+ export declare function passwordlessSetting(store: Store): PasswordlessSetting;
18
+ export declare const passwordlessOn: (store: Store) => boolean;
19
+ /** An operator's act (the caller proves it: on takes a typed password, off is one click). The ledger keeps before → after. */
20
+ export declare function setPasswordless(store: Store, on: boolean, by: string, now: Date): {
21
+ changed: boolean;
22
+ };
23
+ export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
@@ -0,0 +1,26 @@
1
+ export const PASSWORDLESS_ACTION = "passwordless changed";
2
+ /** Read live at every act: a change takes effect on the next one. */
3
+ export function passwordlessSetting(store) {
4
+ // Fail closed: a setting that can't be read is off (a step-up then asks for the password, as it always did).
5
+ let row;
6
+ try {
7
+ row = store.handle.prepare("SELECT actor, at, outcome FROM action_ledger WHERE repo IS NULL AND task_id IS NULL AND action = ? ORDER BY id DESC LIMIT 1").get(PASSWORDLESS_ACTION);
8
+ }
9
+ catch {
10
+ return { on: false, setBy: null, at: null };
11
+ }
12
+ if (row === undefined || (row["outcome"] !== "on" && row["outcome"] !== "off"))
13
+ return { on: false, setBy: null, at: null };
14
+ return { on: row["outcome"] === "on", setBy: String(row["actor"]), at: String(row["at"]) };
15
+ }
16
+ export const passwordlessOn = (store) => passwordlessSetting(store).on;
17
+ /** An operator's act (the caller proves it: on takes a typed password, off is one click). The ledger keeps before → after. */
18
+ export function setPasswordless(store, on, by, now) {
19
+ if (passwordlessOn(store) === on)
20
+ return { changed: false };
21
+ store.recordAction({ at: now.toISOString(), actor: by, repo: null, taskId: null, runId: null, action: PASSWORDLESS_ACTION, outcome: on ? "on" : "off", source: "policy",
22
+ detail: on ? "Passwordless on: signing in is the only password" : "Passwordless off: step-ups ask for the password again" });
23
+ store.bumpWake();
24
+ return { changed: true };
25
+ }
26
+ export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";