toolroll 0.9.63 → 0.9.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
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";
@@ -95,6 +99,7 @@ import { basename, dirname, join, resolve } from "node:path";
95
99
  import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
96
100
  import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
97
101
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
102
+ import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
98
103
  import { authorizePlanUnderMode } from "./plan-auto.js";
99
104
  import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
100
105
  import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
@@ -105,6 +110,7 @@ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync,
105
110
  import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
106
111
  import { claimActor, currentActor, withActor } from "./actor.js";
107
112
  import { parseLeadToken } from "./api-tokens.js";
113
+ import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
108
114
  import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
109
115
  import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
110
116
  import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
@@ -182,7 +188,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
182
188
  import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
183
189
  import { serveMcp } from "./mcp.js";
184
190
  import { createInterface } from "node:readline";
185
- 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";
186
192
  import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
187
193
  import { WorktreePool } from "./worktree.js";
188
194
  import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
@@ -251,6 +257,11 @@ export const OPERATE_HELP = `toolroll — operating the queue
251
257
  toolroll project show show the current project and credential reference
252
258
  toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
253
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)
254
265
  toolroll project concurrency [<n>] --repo <p>
255
266
  how many of a project's tasks build at once (default 2, never past the
256
267
  worker's capacity); an approver changes it with <n>
@@ -752,7 +763,8 @@ export function parseOperateArgs(argv, ownValues = new Set()) {
752
763
  }
753
764
  /** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
754
765
  export async function runOperate(command, argv, write, options = {}) {
755
- 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)));
756
768
  }
757
769
  async function runOperateLocal(command, argv, write, options = {}) {
758
770
  const parsed = parseOperateArgs(argv, command === "flows" ? FLOWS_VALUE_FLAGS : undefined);
@@ -1145,6 +1157,8 @@ async function dispatch(command, positional, flags, context) {
1145
1157
  return projectDeleteCommand(positional, flags, context);
1146
1158
  if (positional[0] === "concurrency")
1147
1159
  return projectConcurrencyCommand(positional, flags, context);
1160
+ if (positional[0] === "network")
1161
+ return projectNetworkCommand(positional, flags, context);
1148
1162
  if (positional[0] === "checks")
1149
1163
  return projectChecksCommand(positional, flags, context);
1150
1164
  if (positional[0] === "demo")
@@ -1223,6 +1237,8 @@ async function dispatch(command, positional, flags, context) {
1223
1237
  case "flows":
1224
1238
  return flowsCommand(positional, flags, context);
1225
1239
  case "config":
1240
+ if (positional[0] === "unconfined-agents")
1241
+ return unconfinedAgentsCommand(positional, flags, context);
1226
1242
  return configCommand(positional, flags, context);
1227
1243
  case "chat":
1228
1244
  return chatCommand(flags, context);
@@ -1243,6 +1259,8 @@ async function dispatch(command, positional, flags, context) {
1243
1259
  });
1244
1260
  case "keys":
1245
1261
  return keysCommand(positional, flags, context);
1262
+ case "agents":
1263
+ return agentsCommand(positional, flags, context);
1246
1264
  case "setup":
1247
1265
  return setupCommand(positional, flags, context);
1248
1266
  case "verify":
@@ -1488,6 +1506,9 @@ function claimCommand(positional, flags, context) {
1488
1506
  if (result.reason === "coordinator-filed") {
1489
1507
  return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
1490
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
+ }
1491
1512
  return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
1492
1513
  }
1493
1514
  // Taking a task is what makes it running; leaving that to the caller would
@@ -1606,7 +1627,7 @@ async function runnerCommand(positional, flags, context) {
1606
1627
  if (acting === null) {
1607
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);
1608
1629
  }
1609
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1630
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1610
1631
  if (!authenticated.ok) {
1611
1632
  return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1612
1633
  }
@@ -1732,7 +1753,7 @@ async function runnerCommand(positional, flags, context) {
1732
1753
  if (acting === null) {
1733
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);
1734
1755
  }
1735
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1756
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1736
1757
  if (!authenticated.ok) {
1737
1758
  return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1738
1759
  }
@@ -1818,7 +1839,7 @@ async function coordinatorCommand(positional, flags, context) {
1818
1839
  if (acting === null) {
1819
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);
1820
1841
  }
1821
- const authenticated = authenticateApprover(store, acting.name, acting.token);
1842
+ const authenticated = credentialApprover(store, acting.name, acting.token);
1822
1843
  if (!authenticated.ok) {
1823
1844
  return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
1824
1845
  }
@@ -2292,6 +2313,17 @@ async function tickCommand(flags, context) {
2292
2313
  root: pool,
2293
2314
  ...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
2294
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
+ }
2295
2327
  // Where a first attempt's branch grows from, resolved once per pass. A
2296
2328
  // detached HEAD refuses the pass rather than guessing: an unattended commit
2297
2329
  // onto "wherever the operator happened to be" is not a default.
@@ -2616,6 +2648,20 @@ async function tickCommand(flags, context) {
2616
2648
  : [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
2617
2649
  let unattestedLane = null;
2618
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
+ }
2619
2665
  if (attestationOf(candidate) === null)
2620
2666
  continue;
2621
2667
  let verdict = attestedThisPass.get(candidate);
@@ -2646,7 +2692,9 @@ async function tickCommand(flags, context) {
2646
2692
  // budget is raised (the 100% alert went out; the task page and the spend page say why).
2647
2693
  // Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
2648
2694
  // Each provider as it bills now.
2649
- 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]));
2650
2698
  if (budgeted.over !== null) {
2651
2699
  dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
2652
2700
  continue;
@@ -3900,6 +3948,7 @@ async function startConsole(options) {
3900
3948
  ...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
3901
3949
  ...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
3902
3950
  ...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
3951
+ ...(options.instanceSecret === undefined ? {} : { instanceSecret: options.instanceSecret }),
3903
3952
  });
3904
3953
  await new Promise((resolve, reject) => {
3905
3954
  server.once("error", reject);
@@ -4035,6 +4084,8 @@ async function requeueTask(positional, flags, context) {
4035
4084
  claimed: `a runner holds ${id} right now — wait for the attempt to end, or stop it`,
4036
4085
  "accepted-result": `${id}'s last result was accepted; change it through a revision, not a rerun`,
4037
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`,
4038
4089
  };
4039
4090
  return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
4040
4091
  }
@@ -4317,6 +4368,13 @@ async function providersCommand(flags, context) {
4317
4368
  // that serializes four of them reads as a hang (Phase 3).
4318
4369
  const probed = await Promise.all(PROVIDER_IDS.map(async (id) => {
4319
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
+ }
4320
4378
  // A version probe carries no key (Codex gemini verify, finding 2).
4321
4379
  const version = await probe(facts.binary, ["--version"], { timeoutMs: 5_000, omitEnv: ALL_CREDENTIAL_ENV });
4322
4380
  const installed = version.code === 0 && !version.notFound;
@@ -4373,7 +4431,7 @@ async function providersCommand(flags, context) {
4373
4431
  if (runnerName === undefined || runnerToken === undefined) {
4374
4432
  return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
4375
4433
  }
4376
- 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));
4377
4435
  const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
4378
4436
  if (!reported.ok) {
4379
4437
  return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
@@ -4444,6 +4502,83 @@ async function providersCommand(flags, context) {
4444
4502
  * own — these are 0600 files under the operator's home, and whoever can
4445
4503
  * write them already owns the machine's spend.
4446
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
+ }
4447
4582
  async function keysCommand(positional, flags, context) {
4448
4583
  const { write, json } = context;
4449
4584
  const [action, provider, ...rest] = positional;
@@ -4451,7 +4586,8 @@ async function keysCommand(positional, flags, context) {
4451
4586
  return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
4452
4587
  }
4453
4588
  if (action === "status") {
4454
- 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 => ({
4455
4591
  provider: one,
4456
4592
  envName: PROVIDER_KEY_ENV[one],
4457
4593
  ...keyStatus(one),
@@ -4475,7 +4611,10 @@ async function keysCommand(positional, flags, context) {
4475
4611
  return EXIT.ok;
4476
4612
  }
4477
4613
  if (provider === undefined || !isProviderId(provider)) {
4478
- 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);
4479
4618
  }
4480
4619
  if (action === "clear") {
4481
4620
  const cleared = clearProviderKey(provider);
@@ -4580,7 +4719,8 @@ async function peopleCommand(positional, flags, context) {
4580
4719
  if (acting === null) {
4581
4720
  return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
4582
4721
  }
4583
- 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));
4584
4724
  if (!authenticated.ok) {
4585
4725
  return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
4586
4726
  }
@@ -4647,7 +4787,8 @@ async function chatApprovalCommand(positional, flags, context) {
4647
4787
  const acting = await askCredentials(flags, context);
4648
4788
  if (acting === null)
4649
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);
4650
- 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));
4651
4792
  if (!authed.ok)
4652
4793
  return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
4653
4794
  const given = text(flags, "repo");
@@ -4710,7 +4851,8 @@ async function modeCommand(positional, flags, context) {
4710
4851
  if (acting === null) {
4711
4852
  return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
4712
4853
  }
4713
- 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));
4714
4856
  if (!authed.ok) {
4715
4857
  return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
4716
4858
  }
@@ -6816,7 +6958,7 @@ async function resolveUpApprover(context, flags, canPrompt) {
6816
6958
  return { approver: selected, approvers: names, verified, passwordFile, mintedPassword: null, notes };
6817
6959
  }
6818
6960
  /** The platform browser opener — detached, silent, never load-bearing. */
6819
- function openBrowser(url) {
6961
+ export function openBrowser(url) {
6820
6962
  // TOOLROLL_BROWSER_COMMAND names another program to open it with (it gets the address as its one argument).
6821
6963
  const chosen = envValue(process.env, "BROWSER_COMMAND");
6822
6964
  const command = chosen !== undefined && chosen !== "" ? [chosen, url]
@@ -7182,9 +7324,12 @@ async function upCommand(flags, context) {
7182
7324
  // proved set on each request, so its project rail and unified chat move
7183
7325
  // with the live builder instead of freezing at process start.
7184
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;
7185
7329
  let console_;
7186
7330
  try {
7187
7331
  console_ = await startConsole({
7332
+ instanceSecret: () => instanceSecret,
7188
7333
  context,
7189
7334
  host: hostFlag,
7190
7335
  port,
@@ -7442,7 +7587,7 @@ async function upCommand(flags, context) {
7442
7587
  const observeReadiness = () => {
7443
7588
  if (context.agentRunner !== undefined)
7444
7589
  return;
7445
- 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))
7446
7591
  .then(observed => {
7447
7592
  if (!stopping)
7448
7593
  reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, clock());
@@ -7474,6 +7619,21 @@ async function upCommand(flags, context) {
7474
7619
  login: { account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver, file: approverPlan2.passwordFile },
7475
7620
  });
7476
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`);
7477
7637
  if (fatal === null) {
7478
7638
  if (json) {
7479
7639
  write(envelopeJson({
@@ -7541,7 +7701,15 @@ async function upCommand(flags, context) {
7541
7701
  process.removeListener("SIGINT", stop);
7542
7702
  process.removeListener("SIGTERM", stop);
7543
7703
  retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
7544
- 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;
7545
7713
  if (fatal !== null) {
7546
7714
  // The startup envelope (when json) already went out; the exit code is
7547
7715
  // the health signal (finding 11/20) — never a second envelope.
@@ -7671,7 +7839,7 @@ async function bridgeCommand(positional, flags, context) {
7671
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);
7672
7840
  }
7673
7841
  const { name: asWho, token } = acting;
7674
- const authenticated = authenticateApprover(store, asWho, token);
7842
+ const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
7675
7843
  if (!authenticated.ok) {
7676
7844
  return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
7677
7845
  }
@@ -9451,7 +9619,7 @@ async function leadCommand(positional, flags, context) {
9451
9619
  if (currentActor()?.lead === true)
9452
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);
9453
9621
  const acting = await askCredentials(flags, context);
9454
- 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));
9455
9623
  if (acting === null || verified === null || !verified.ok) {
9456
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);
9457
9625
  }
@@ -10365,6 +10533,10 @@ function credentialsFrom(flags, context) {
10365
10533
  if (remembered !== null && (name === undefined || name === remembered.name)) {
10366
10534
  name = remembered.name;
10367
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 };
10368
10540
  }
10369
10541
  }
10370
10542
  return { name, token };
@@ -10385,6 +10557,12 @@ async function askCredentials(flags, context) {
10385
10557
  * which scope.ts's authenticateAccount takes as that person (a viewer for a read token) and never checks as a
10386
10558
  * password; these add the token's own project limit.
10387
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
+ }
10388
10566
  function authenticateApprover(store, by, token, repo) {
10389
10567
  const remote = activeRemote();
10390
10568
  if (remote !== null && repo !== undefined && !remote.allows(repo))
@@ -10734,6 +10912,86 @@ async function projectRulesCommand(positional, flags, context) {
10734
10912
  context.store.setApprovalRules(repo, next, acting.name, context.clock());
10735
10913
  return succeed(context.write, context.json, command, { repo, rules: next }, () => [`${repo}: ${rulesSummary(next)}`]);
10736
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
+ }
10737
10995
  /** `project concurrency [<n>] --repo <p>`: how many of a project's tasks build at once. Changing it is an approver's act. */
10738
10996
  async function projectConcurrencyCommand(positional, flags, context) {
10739
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.";
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * One pane per open run: its task, worker, role, stage word, elapsed
5
5
  * clock, and the tail of its live transcript (the same per-run file the
6
- * console's run page follows: text the agent said and the kind of tool it
7
- * reached for — never file contents, never a command line, credential
8
- * shapes redacted at write). Digits focus one pane, `a` shows all, `q`
6
+ * console's run page follows: what the agent said and each step it took —
7
+ * a command's text only where its project shows commands, never file
8
+ * contents, credential shapes redacted at write and again at read). Digits focus one pane, `a` shows all, `q`
9
9
  * leaves. No dependency: the screen is the alternate buffer and a
10
10
  * handful of escape sequences. `--tmux` opens a real tmux session with one
11
11
  * window per run for people who live there. Outside a TTY (or with
@@ -36,8 +36,16 @@ type Tail = {
36
36
  offset: number;
37
37
  lines: string[];
38
38
  state: PeekPane["transcript"];
39
+ gen?: string;
40
+ inKey?: boolean;
41
+ keyTail?: string;
39
42
  };
40
- /** Advance one run's tail from its offset; `replaced` resets visibly. */
43
+ /**
44
+ * Advance one run's tail from its offset; `replaced` resets visibly. The
45
+ * offset always moves by the bytes read, even when nothing in them shows
46
+ * (a finished step, a skipped line), and an offset is only ever used
47
+ * against the file it came from.
48
+ */
41
49
  export declare function advanceTail(root: string, runId: number, tail: Tail): Tail;
42
50
  /** The stage word for a pane: the machine's own phase, or what precedes it. */
43
51
  export declare function stageWord(run: {