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/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.65 — 2026-10-22
4
+
5
+ 0.9.64 was tagged but never published: a macOS check failed on Node 24.
6
+ 0.9.65 is the first published release with everything listed under 0.9.64
7
+ below, plus:
8
+
9
+ - **The release check's upgrade test** skips a tagged release that never
10
+ reached npm, instead of failing to install it.
11
+ - **A Linux build's network bridge always fits a socket path.** When the
12
+ plane's own folder is too deep for one, the bridge goes in the system temp
13
+ folder, which a confined Linux build never sees (its `/tmp` is its own).
14
+
15
+ ## 0.9.64 — 2026-10-22
16
+
17
+ **Upgrading:** builds now reach only the network their approval allows
18
+ (below). On a Mac, a project whose tests start a local server needs
19
+ **Settings → Project → Network → local services** turned on. A machine with
20
+ no OS sandbox (Windows, Linux without bubblewrap) no longer runs builds
21
+ unless an instance operator allows it (`toolroll config unconfined-agents`).
22
+
23
+ - **Builds reach only their approved network.** Each approved task seals
24
+ what its build may reach: the agent's provider, the package registries
25
+ the project uses, and hosts you add in **Settings → Project → Network**
26
+ (or `toolroll project network`). The OS sandbox refuses everything else,
27
+ name lookups included; a result's **Network** row shows what the build
28
+ connected to and what was refused.
29
+ - **Agents you can watch.** A light cockpit with each task's agent lanes,
30
+ the live conversation in time order, and real terminals for running
31
+ builds.
32
+ - **ACP agents.** OpenCode, Cursor, Cline, Qwen and Goose can build, each
33
+ checked on this machine before it takes work.
34
+ - **Passwordless mode.** Signing in is the only password; credential
35
+ changes still ask for it.
36
+ - **One page per task and agent**, with Activity, Result and Plan tabs and
37
+ one ⋯ menu.
38
+ - **Lead sweeps.** One card and one yes start many isolated tasks, tracked
39
+ in one table.
40
+ - **The lead queue.** Messages sent while the lead answers wait their turn.
41
+ - **Lead tools on demand**, with prompt caching: smaller, cheaper turns.
42
+ - **How it ran.** Every result shows time, turns, tokens and cache use.
43
+ - **Signed webhooks become tasks.**
44
+ - **Phone keyboard.** Chat and text boxes stay above it; Return writes a
45
+ new line on a phone.
46
+ - **A calmer UI**: one button style, theme tokens, status glyphs and a Crew
47
+ rail.
48
+ - **`toolroll release` finishes**: a release build is recorded as a commit of
49
+ its own holding exactly the candidate's files; the release now accepts it
50
+ by its files and deploys to it, instead of stopping at the check.
51
+ - **Release check** sizes itself to the machine; deploys keep hidden table
52
+ ids and find dependencies behind macOS's temp-folder link.
53
+
54
+ ## 0.9.63 — 2026-10-13
55
+
56
+ - **Scheduled flows run on their own.** Sign a flow once with limits
57
+ (runs a day, budget, end date); its on-time firings build without a
58
+ person, and the script it runs is checked again when it fires.
59
+ - **Secrets in the OS keychain.** Tool keys, the Linear key, Google mail and
60
+ flow secrets move into the macOS Keychain or Linux Secret Service, never
61
+ argv or the database. OAuth tools give each run a short-lived token, never
62
+ the refresh token. Settings shows how each tool's runs get credentials.
63
+ - **Builds stay in their own checkout.** A build can't read or write another
64
+ project's or another task's checkout, and a checkout swapped for a link
65
+ is refused at the next launch.
66
+ - **Safer cleanup.** Removing finished checkouts runs git with hooks,
67
+ filters and nested-repo config switched off.
68
+ - **Attack suite.** Every bypass found in review is a permanent test, run by
69
+ the Quick check whenever sandbox or secrets code changes.
70
+ - **Tool secrets stay outside the build.** A broker outside the sandbox
71
+ holds them. Codex refuses tools that carry secrets. Shared dependencies and
72
+ `git gc` work inside the fence.
73
+ - **Tighter sandbox.** Builds can no longer rename folders above protected
74
+ paths or inspect other processes' memory; tool servers get only the
75
+ environment they need.
76
+ - **Tools with secrets need the macOS sandbox.** A build without it, and
77
+ every build on Linux, leaves those tools out and says why.
78
+ - **`toolroll doctor`.** Checks that Toolroll's service holds no key a build
79
+ could read, and lists the steps to fix it. It reads only.
80
+ - **Research tools answer every refusal**, even when the service accepts the
81
+ rest with an empty reply.
82
+
3
83
  ## 0.9.62 — 2026-10-12
4
84
 
5
85
  - **Release checks in minutes.** The upgrade path runs on a sample, CI's
package/README.md CHANGED
@@ -409,13 +409,13 @@ do not loosen when you leave the room:
409
409
  | touch a default branch | builds land on `toolroll/<task>` in a leased worktree; push + PR happen only under a publication grant naming the exact repo, branch prefix, and base |
410
410
  | approve its own work | approval nonces are minted only on screens that restate the digest-bound terms, and require your approver token typed again — **no LLM sits in any approval path** — and the [agent fence](#what-an-agent-can-reach) keeps your remembered login, runner tokens and the database out of the agent's reach |
411
411
  | act on an irreversible option | `reversible` is a schema field; irreversible choices never auto-apply, and answering one from a phone takes a second minted confirmation tap |
412
- | see Toolroll's secrets | the [agent fence](#what-an-agent-can-reach) blocks your login, runner and bot tokens, stored provider keys and project tool secrets at the operating system; other providers' keys are stripped from each agent's environment; secrets live in 0600 files, never in the database, URLs, or logs |
412
+ | see Toolroll's secrets | the [agent fence](#what-an-agent-can-reach) blocks your login, runner and bot tokens, stored provider keys and project tool secrets at the operating system; other providers' keys are stripped from each agent's environment; tool secrets rest sealed in the OS keychain ([how](#how-build-sessions-are-protected)) and other secrets in 0600 files, never in the database, URLs, or logs |
413
413
  | spend while idle | **an LLM never polls** — the daemon does every no-judgement chore at zero token cost and wakes an agent only on a real event |
414
414
  | spend without being counted | every provider spawn is stamped *before* it spends, so cost is measured, never asserted |
415
415
  | guess at a judgement call | it parks a typed decision — recap, options with reversibility, recommendation, evidence — and the other eleven tasks keep going |
416
416
 
417
417
  The whole claim is executable: one test,
418
- [`src/unattended.test.ts`](src/unattended.test.ts), queues twelve tasks,
418
+ [`src/unattended.slow.test.ts`](src/unattended.slow.test.ts), queues twelve tasks,
419
419
  walks away, and comes back to pull requests.
420
420
 
421
421
  The name comes from a captain's night orders — the written standing instructions left for the officer of the watch: *proceed on this course without me, and wake me under exactly these conditions.* That is the product, and it is not about the hour: it is for **long-running work that outlasts your attention** — an afternoon of errands, a weekend, or yes, a night.
@@ -572,17 +572,75 @@ runner and coordinator tokens, chat bot tokens, the database and its backups,
572
572
  other runs' evidence) — everything there except the build's own worktree —
573
573
  and `~/.toolroll` (stored provider keys and project tool secrets).
574
574
 
575
+ Seatbelt and bubblewrap deny the whole checkout pool and allow the build’s own
576
+ checkout back, covering other projects created after launch too. Codex keeps
577
+ the own checkout’s ancestors traversable and denies each other project folder
578
+ found in trusted checkout pools at launch. A project added after a Codex build
579
+ starts is covered on the next launch. Own Git data, linked shared dependencies
580
+ and launch tools remain available.
581
+
575
582
  | | Auto | Full access |
576
583
  |---|---|---|
577
- | **Codex / OpenRouter** (any OS) | Codex's workspace sandbox (workspace writes, no network) with the fence | Codex's sandbox widened to write anywhere with network, with the fence |
578
- | **Claude** | macOS: the agent runs inside a sandbox that denies the fence. Linux/Windows: Claude's own file tools refuse the fence, but its shell is not fenced yet | same |
579
- | **Gemini** | macOS: fenced like Claude. Linux/Windows: not fenced yet | same |
584
+ | **Codex / OpenRouter** (any OS) | Codex's workspace sandbox (workspace writes, no network) with the fence | Codex's sandbox widened to write anywhere, with the fence; its commands reach the network only when the project's network is open |
585
+ | **Claude, Gemini, ACP agents** | macOS: the agent runs inside a Seatbelt sandbox that denies the fence. Linux: inside bubblewrap. Neither (Windows, or Linux without bubblewrap): the build doesn't start, unless an instance operator allows unconfined agents | same |
586
+
587
+ **The network.** Each approved scope says what its build may reach: the
588
+ agent's provider, the package registries the project uses (npm, PyPI,
589
+ crates.io, the Go proxy, RubyGems, Maven), and any hosts an approver added
590
+ with `toolroll project network --repo <p> --allow api.example.com`. Every
591
+ connection goes through the run's own proxy, which admits only those hosts;
592
+ the sandbox refuses everything else, name lookups included. On a Mac a build
593
+ reaches no local service but its own proxy and tools: a project whose tests
594
+ start a local server sets `--local any` (every local service on that Mac then
595
+ becomes reachable). A result's **Network** row says what the build connected
596
+ to and what was refused.
580
597
 
581
598
  Each attempt records how it was fenced. Two limits to know: in **API-key
582
599
  mode** an agent's own provider key is in its environment, so its shell can
583
600
  read it (subscription mode, the default, puts no key there); and a project's
584
- own tool secrets reach that project's tools. Reviews and the lead chat run
585
- with no tools and are confined to their own files. See [SECURITY.md](SECURITY.md).
601
+ own tool secrets reach that project's tools. They stay in a per-run broker
602
+ outside the fence: the agent gets only its address and a token that stops
603
+ working when the run ends, and on macOS it can't read another process's
604
+ arguments or environment. Reviews and the lead chat run with no tools and are
605
+ confined to their own files. See [SECURITY.md](SECURITY.md).
606
+
607
+ ### How build sessions are protected
608
+
609
+ A build runs as your own user, so Toolroll keeps the secrets it holds (tool
610
+ keys and sign-ins, the Linear key, a Google mail connection, flow webhook
611
+ secrets) away from it in layers:
612
+
613
+ - **The fence.** The operating system denies the build Toolroll's own
614
+ folders, your home configuration and other projects (the table above).
615
+ - **The broker.** A run's tools are served by a broker in Toolroll's own
616
+ process, outside the fence. The agent gets only the broker's local address
617
+ and a key for that run, which stops working when the run ends. Another
618
+ run's key opens nothing.
619
+ - **The keychain.** At rest, secrets are in the macOS Keychain (through
620
+ `security`, with the value on its input) or the Linux Secret Service (the
621
+ system's `secret-tool`, value on its input). A fenced build can still ask
622
+ the keychain for an item, so each is sealed with a key in `tool-secrets`
623
+ beside Toolroll's database. Toolroll makes that folder when it starts,
624
+ before any build, and records which key it made; a folder, key or link put
625
+ there by anything else is refused. What a build reads is ciphertext.
626
+ Secrets are read only when a run starts. Files from before move into the
627
+ keychain the first time they are used, and the plaintext copy is removed.
628
+ Where there is no keychain, or the keyring doesn't answer, secrets stay in
629
+ owner-only files, and Settings and `toolroll integrations` say so.
630
+ - **Short-lived tokens.** A service connected by signing in gives each run
631
+ a fresh access token of its own, made outside the fence when the run starts
632
+ and limited to read access for research steps where the service allows.
633
+ The refresh token never leaves Toolroll, and one renewal runs at a time
634
+ across Toolroll's processes. If a renewed sign-in can't be saved, the run
635
+ doesn't start and every token made for it is revoked. The run's token is
636
+ revoked when the run ends, or runs out on its own where the service has no
637
+ way to revoke it. Keys you paste in stay in the broker. Settings → Tools
638
+ shows which each tool uses.
639
+
640
+ What remains: any program of yours running outside the fence (not started by
641
+ a build) can read what you can, including Toolroll's folder and the keychain.
642
+ Toolroll protects your secrets from its builds, not from other software you
643
+ run.
586
644
 
587
645
  Every new-task and task-scope form has the same two-choice control. A task's
588
646
  choice is durable through planning rewrites and is sealed into the approved
@@ -1081,7 +1139,7 @@ doubling backoff, three-strike stalls a person exits with `task requeue` —
1081
1139
  and every provider spawn is stamped before it spends, so cost is measured,
1082
1140
  never asserted. CI on published PRs is watched as episodes that never call
1083
1141
  silence green. The whole unattended stretch is one test,
1084
- [`src/unattended.test.ts`](src/unattended.test.ts): queue twelve, walk
1142
+ [`src/unattended.slow.test.ts`](src/unattended.slow.test.ts): queue twelve, walk
1085
1143
  away, come back to PRs. Architecture: [`docs/DESIGN.md`](docs/DESIGN.md);
1086
1144
  the item-by-item ledger: [`docs/PROGRESS.md`](docs/PROGRESS.md).
1087
1145
 
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The coding agents Toolroll runs over the Agent Client Protocol
3
+ * (docs/plans/acp-adapter.md). Each is its own provider id — readiness,
4
+ * sign-in pauses, routing and spend stay per agent — but they share one
5
+ * transport (acp-client.ts), and a row here is the only place an agent is
6
+ * described.
7
+ *
8
+ * Nothing in a row is trusted to be true of the binary on this machine: an
9
+ * agent runs only after its conformance check passed against that exact
10
+ * binary (attestation), and the check is what proves the argv, the name it
11
+ * gives in `initialize`, and the models it offers. Until then it is listed
12
+ * and refused.
13
+ */
14
+ import type { ProviderId } from "./contracts/provider.js";
15
+ export declare const ACP_PROVIDER_IDS: readonly ["opencode", "cursor", "cline", "qwen", "goose"];
16
+ export type AcpProviderId = (typeof ACP_PROVIDER_IDS)[number];
17
+ /** The providers with their own adapter and key: everything that is not an ACP agent. */
18
+ export type KeyedProviderId = Exclude<ProviderId, AcpProviderId>;
19
+ export type AcpAgent = {
20
+ /** How people know it. */
21
+ name: string;
22
+ /** The command looked up on PATH. */
23
+ binary: string;
24
+ /** What starts its ACP server on stdio. */
25
+ argv: readonly string[];
26
+ /** What the operator runs to sign in. */
27
+ signIn: string;
28
+ /** Folders under the home directory that hold its own configuration and sign-in (fenced like other agents' homes). */
29
+ home: readonly string[];
30
+ /** What still reaches a run beside the brief: its own configuration surfaces, in words. */
31
+ configSurface: readonly string[];
32
+ };
33
+ export declare const ACP_AGENTS: Readonly<Record<AcpProviderId, AcpAgent>>;
34
+ export declare function isAcpProvider(provider: string): provider is AcpProviderId;
35
+ /** Whether a profile (or anything carrying a provider) is an ACP agent's: narrows the union. */
36
+ export declare function isAcpProfile<P extends {
37
+ provider: string;
38
+ }>(profile: P): profile is Extract<P, {
39
+ provider: AcpProviderId;
40
+ }>;
41
+ export declare function isKeyedProvider(provider: ProviderId): provider is KeyedProviderId;
42
+ /** Build a per-provider record whose ACP entries all come from one function of the agent's row. */
43
+ export declare function withAcp<K extends KeyedProviderId, V>(keyed: Record<K, V>, acp: (agent: AcpAgent, provider: AcpProviderId) => V): Record<K | AcpProviderId, V>;
@@ -0,0 +1,62 @@
1
+ export const ACP_PROVIDER_IDS = ["opencode", "cursor", "cline", "qwen", "goose"];
2
+ export const ACP_AGENTS = {
3
+ opencode: {
4
+ name: "OpenCode",
5
+ binary: "opencode",
6
+ // --pure: "run without external plugins" (opencode 1.18 `acp --help`).
7
+ argv: ["acp", "--pure"],
8
+ signIn: "opencode auth login",
9
+ home: [".config/opencode", ".local/share/opencode"],
10
+ configSurface: ["~/.config/opencode (models, providers, permissions)", "repository AGENTS.md and opencode.json", "its own sign-ins (opencode auth login)"],
11
+ },
12
+ cursor: {
13
+ name: "Cursor",
14
+ binary: "cursor-agent",
15
+ argv: ["acp"],
16
+ signIn: "cursor-agent login",
17
+ home: [".cursor"],
18
+ configSurface: ["~/.cursor (rules, MCP servers)", "repository .cursor/rules and AGENTS.md", "its own sign-in"],
19
+ },
20
+ cline: {
21
+ name: "Cline",
22
+ binary: "cline",
23
+ argv: ["--acp"],
24
+ signIn: "cline auth",
25
+ home: [".cline"],
26
+ configSurface: ["~/.cline (settings, MCP servers)", "repository .clinerules", "its own sign-in"],
27
+ },
28
+ qwen: {
29
+ name: "Qwen Code",
30
+ binary: "qwen",
31
+ argv: ["--acp"],
32
+ signIn: "qwen (then /auth)",
33
+ home: [".qwen"],
34
+ configSurface: ["~/.qwen/settings.json (MCP servers, model settings)", "QWEN.md (global and repository)", "its own sign-in"],
35
+ },
36
+ goose: {
37
+ name: "Goose",
38
+ binary: "goose",
39
+ argv: ["acp"],
40
+ signIn: "goose configure",
41
+ home: [".config/goose"],
42
+ configSurface: ["~/.config/goose (providers, extensions)", "repository .goosehints", "its own sign-in"],
43
+ },
44
+ };
45
+ const ACP = new Set(ACP_PROVIDER_IDS);
46
+ export function isAcpProvider(provider) {
47
+ return ACP.has(provider);
48
+ }
49
+ /** Whether a profile (or anything carrying a provider) is an ACP agent's: narrows the union. */
50
+ export function isAcpProfile(profile) {
51
+ return ACP.has(profile.provider);
52
+ }
53
+ export function isKeyedProvider(provider) {
54
+ return !ACP.has(provider);
55
+ }
56
+ /** Build a per-provider record whose ACP entries all come from one function of the agent's row. */
57
+ export function withAcp(keyed, acp) {
58
+ const out = { ...keyed };
59
+ for (const provider of ACP_PROVIDER_IDS)
60
+ out[provider] = acp(ACP_AGENTS[provider], provider);
61
+ return out;
62
+ }
@@ -0,0 +1,44 @@
1
+ /** What makes an interpreter load code beside the script it was given: never passed to an ACP launch. */
2
+ export declare const INTERPRETER_INJECTION_ENV: readonly string[];
3
+ /** Bumped whenever the conformance probe changes what it proves: every attestation made under another version is void. */
4
+ export declare const ACP_CONFORMANCE_VERSION = 2;
5
+ export type FileBinding = {
6
+ role: "launcher" | "interpreter" | "package" | "entry";
7
+ path: string;
8
+ sha256: string;
9
+ dev: number;
10
+ ino: number;
11
+ ctimeMs: number;
12
+ size: number;
13
+ };
14
+ /** How the attested binary is started, and every file that start depends on. */
15
+ export type AcpLaunch = {
16
+ /** What is executed: the launcher itself, or the pinned interpreter. */
17
+ file: string;
18
+ /** Arguments before the agent's own ACP argv: a script's shebang arguments and the script itself. */
19
+ prefix: string[];
20
+ bindings: FileBinding[];
21
+ };
22
+ /** sha256 of a regular file, read without following a final symlink. */
23
+ export declare function hashFile(path: string): string | null;
24
+ /** The interpreter and its arguments a shebang line names — `#!/abs/interp [arg]` or `#!/usr/bin/env [-S] name [args]` — or null. */
25
+ export declare function parseShebang(line: string, path: string): {
26
+ interpreter: string;
27
+ args: string[];
28
+ } | null;
29
+ /**
30
+ * How to launch a binary found at `launcher` (a realpath), with every file bound — or why it can't be. `path` is the
31
+ * PATH a `#!/usr/bin/env name` shebang is resolved on, once, here.
32
+ */
33
+ export declare function launchOf(launcher: string, path: string): AcpLaunch | {
34
+ problem: string;
35
+ };
36
+ /** Whether every bound file is still the one that passed: same stat, or on any change the same sha256. */
37
+ export declare function verifyLaunch(launch: AcpLaunch): {
38
+ ok: true;
39
+ } | {
40
+ ok: false;
41
+ problem: string;
42
+ };
43
+ /** A stored launch, read strictly: anything this code never writes reads as null. */
44
+ export declare function launchFromJson(json: string): AcpLaunch | null;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * An ACP agent's admission (docs/plans/acp-adapter.md § Admission): it runs only on a passed conformance check of the
3
+ * exact binary on this runner, recorded as an attestation. Absence is denial.
4
+ *
5
+ * The record says how to launch that binary — a native launcher directly, a script through the interpreter its
6
+ * shebang named, resolved once at the check and pinned (the child's PATH never chooses it) — and binds every file
7
+ * that launch depends on: the launcher, the interpreter, and for a package its package.json and entry. Each binding
8
+ * keeps the file's stat tuple and sha256. At spawn, after all asynchronous setup, every binding is checked again: a
9
+ * changed stat is re-hashed, and the spawn refuses unless every hash still matches.
10
+ *
11
+ * What it proves, exactly: the bytes of those files are the bytes that passed the check, as of the re-check
12
+ * immediately before spawn. Not an exclusive lock on the paths: a writer racing the narrow window between that re-check
13
+ * and exec is not excluded (Node has no exec-by-descriptor), the same residual the version attestation states. Nor the
14
+ * agent's other files (a bundle's chunks, its plugins): those are its configuration surface. The environment
15
+ * variables that make an interpreter load other code are shed at every ACP launch (INTERPRETER_INJECTION_ENV).
16
+ */
17
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import { delimiter, dirname, isAbsolute, join } from "node:path";
20
+ /** What makes an interpreter load code beside the script it was given: never passed to an ACP launch. */
21
+ export const INTERPRETER_INJECTION_ENV = [
22
+ "NODE_OPTIONS", "NODE_PATH", "NODE_REPL_EXTERNAL_MODULE", "PYTHONPATH", "PYTHONSTARTUP", "PYTHONHOME", "PYTHONINSPECT",
23
+ "RUBYOPT", "RUBYLIB", "PERL5OPT", "PERL5LIB", "PERLLIB", "BUN_OPTIONS", "DENO_DIR", "LD_PRELOAD", "LD_LIBRARY_PATH",
24
+ "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH",
25
+ ];
26
+ /** Bumped whenever the conformance probe changes what it proves: every attestation made under another version is void. */
27
+ export const ACP_CONFORMANCE_VERSION = 2;
28
+ const READ_CAP = 64 * 1024;
29
+ const HASH_CHUNK = 1024 * 1024;
30
+ /** sha256 of a regular file, read without following a final symlink. */
31
+ export function hashFile(path) {
32
+ let fd = null;
33
+ try {
34
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | (constants.O_NONBLOCK ?? 0));
35
+ if (!fstatSync(fd).isFile())
36
+ return null;
37
+ const hash = createHash("sha256");
38
+ const buffer = Buffer.alloc(HASH_CHUNK);
39
+ for (;;) {
40
+ const read = readSync(fd, buffer, 0, HASH_CHUNK, null);
41
+ if (read === 0)
42
+ break;
43
+ hash.update(buffer.subarray(0, read));
44
+ }
45
+ return hash.digest("hex");
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ finally {
51
+ if (fd !== null)
52
+ closeSync(fd);
53
+ }
54
+ }
55
+ function head(path, cap) {
56
+ let fd = null;
57
+ try {
58
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | (constants.O_NONBLOCK ?? 0));
59
+ if (!fstatSync(fd).isFile())
60
+ return null;
61
+ const buffer = Buffer.alloc(cap);
62
+ return buffer.subarray(0, readSync(fd, buffer, 0, cap, 0));
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ finally {
68
+ if (fd !== null)
69
+ closeSync(fd);
70
+ }
71
+ }
72
+ function bind(role, path) {
73
+ try {
74
+ const stat = statSync(path);
75
+ if (!stat.isFile())
76
+ return null;
77
+ const sha256 = hashFile(path);
78
+ if (sha256 === null)
79
+ return null;
80
+ return { role, path, sha256, dev: stat.dev, ino: stat.ino, ctimeMs: stat.ctimeMs, size: stat.size };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function isNative(bytes) {
87
+ if (bytes.length < 4)
88
+ return false;
89
+ const magic = bytes.readUInt32BE(0);
90
+ return magic === 0x7f454c46 || [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca].includes(magic);
91
+ }
92
+ /** A name found on a PATH, by realpath; only absolute PATH entries count. */
93
+ function onPath(name, path) {
94
+ for (const folder of path.split(delimiter)) {
95
+ if (folder === "" || !isAbsolute(folder))
96
+ continue;
97
+ try {
98
+ const real = realpathSync(join(folder, name));
99
+ const stat = statSync(real);
100
+ if (stat.isFile() && (stat.mode & 0o111) !== 0)
101
+ return real;
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ /** The interpreter and its arguments a shebang line names — `#!/abs/interp [arg]` or `#!/usr/bin/env [-S] name [args]` — or null. */
110
+ export function parseShebang(line, path) {
111
+ if (!line.startsWith("#!"))
112
+ return null;
113
+ const words = line.slice(2).trim().split(/\s+/).filter(one => one !== "");
114
+ const [first, ...rest] = words;
115
+ if (first === undefined || !isAbsolute(first))
116
+ return null;
117
+ if (first === "/usr/bin/env" || first === "/bin/env") {
118
+ const named = rest[0] === "-S" ? rest.slice(1) : rest;
119
+ const [name, ...args] = named;
120
+ if (name === undefined || name.includes("/") || name.startsWith("-") || name.includes("="))
121
+ return null;
122
+ const interpreter = onPath(name, path);
123
+ return interpreter === null ? null : { interpreter, args };
124
+ }
125
+ if (rest.length > 1)
126
+ return null;
127
+ try {
128
+ return { interpreter: realpathSync(first), args: rest };
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ /** The nearest package.json above a script and the file its `bin`/`main` names, when that is not the launcher. */
135
+ function packageBindings(launcher) {
136
+ let folder = dirname(launcher);
137
+ for (let depth = 0; depth < 6; depth += 1) {
138
+ const file = join(folder, "package.json");
139
+ const bytes = head(file, READ_CAP);
140
+ if (bytes !== null) {
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(bytes.toString("utf8"));
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ const pkg = bind("package", file);
149
+ if (pkg === null)
150
+ return null;
151
+ const bins = typeof parsed["bin"] === "string" ? [parsed["bin"]] : parsed["bin"] !== null && typeof parsed["bin"] === "object" ? Object.values(parsed["bin"]) : [];
152
+ const entries = [...bins, parsed["main"]].filter((one) => typeof one === "string");
153
+ const out = [pkg];
154
+ for (const entry of new Set(entries)) {
155
+ let real;
156
+ try {
157
+ real = realpathSync(join(folder, entry));
158
+ }
159
+ catch {
160
+ continue;
161
+ }
162
+ if (real === launcher)
163
+ continue;
164
+ const bound = bind("entry", real);
165
+ if (bound !== null)
166
+ out.push(bound);
167
+ }
168
+ return out;
169
+ }
170
+ const up = dirname(folder);
171
+ if (up === folder)
172
+ break;
173
+ folder = up;
174
+ }
175
+ return [];
176
+ }
177
+ /**
178
+ * How to launch a binary found at `launcher` (a realpath), with every file bound — or why it can't be. `path` is the
179
+ * PATH a `#!/usr/bin/env name` shebang is resolved on, once, here.
180
+ */
181
+ export function launchOf(launcher, path) {
182
+ const bytes = head(launcher, 4096);
183
+ if (bytes === null)
184
+ return { problem: `${launcher} can't be read` };
185
+ const self = bind("launcher", launcher);
186
+ if (self === null)
187
+ return { problem: `${launcher} can't be read` };
188
+ if (isNative(bytes))
189
+ return { file: launcher, prefix: [], bindings: [self] };
190
+ const first = bytes.toString("utf8").split("\n")[0] ?? "";
191
+ const shebang = parseShebang(first, path);
192
+ if (shebang === null)
193
+ return { problem: `${launcher} is neither a native program nor a script whose interpreter can be pinned (unsupported-launcher)` };
194
+ const interpreterBytes = head(shebang.interpreter, 4096);
195
+ if (interpreterBytes === null || !isNative(interpreterBytes))
196
+ return { problem: `${shebang.interpreter}, which runs ${launcher}, is not a native program (unsupported-launcher)` };
197
+ const interpreter = bind("interpreter", shebang.interpreter);
198
+ const pkg = packageBindings(launcher);
199
+ if (interpreter === null || pkg === null)
200
+ return { problem: `the files ${launcher} runs from can't all be read` };
201
+ return { file: shebang.interpreter, prefix: [...shebang.args, launcher], bindings: [self, interpreter, ...pkg] };
202
+ }
203
+ /** Whether every bound file is still the one that passed: same stat, or on any change the same sha256. */
204
+ export function verifyLaunch(launch) {
205
+ for (const binding of launch.bindings) {
206
+ let stat;
207
+ try {
208
+ if (lstatSync(binding.path).isSymbolicLink())
209
+ return { ok: false, problem: `${binding.path} became a link since the check` };
210
+ stat = statSync(binding.path);
211
+ }
212
+ catch {
213
+ return { ok: false, problem: `${binding.path} is gone since the check` };
214
+ }
215
+ if (stat.dev === binding.dev && stat.ino === binding.ino && stat.ctimeMs === binding.ctimeMs && stat.size === binding.size)
216
+ continue;
217
+ if (hashFile(binding.path) !== binding.sha256)
218
+ return { ok: false, problem: `${binding.path} changed since the check — check the agent again` };
219
+ }
220
+ return { ok: true };
221
+ }
222
+ /** A stored launch, read strictly: anything this code never writes reads as null. */
223
+ export function launchFromJson(json) {
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(json);
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
232
+ return null;
233
+ const value = parsed;
234
+ if (typeof value["file"] !== "string" || !isAbsolute(value["file"]))
235
+ return null;
236
+ if (!Array.isArray(value["prefix"]) || !value["prefix"].every(one => typeof one === "string"))
237
+ return null;
238
+ if (!Array.isArray(value["bindings"]) || value["bindings"].length === 0)
239
+ return null;
240
+ const bindings = [];
241
+ for (const one of value["bindings"]) {
242
+ if (one === null || typeof one !== "object")
243
+ return null;
244
+ const b = one;
245
+ if (!["launcher", "interpreter", "package", "entry"].includes(String(b["role"])))
246
+ return null;
247
+ if (typeof b["path"] !== "string" || !isAbsolute(b["path"]) || typeof b["sha256"] !== "string" || !/^[0-9a-f]{64}$/.test(b["sha256"]))
248
+ return null;
249
+ if (![b["dev"], b["ino"], b["ctimeMs"], b["size"]].every(n => typeof n === "number" && Number.isFinite(n)))
250
+ return null;
251
+ bindings.push({ role: b["role"], path: b["path"], sha256: b["sha256"], dev: b["dev"], ino: b["ino"], ctimeMs: b["ctimeMs"], size: b["size"] });
252
+ }
253
+ if (bindings[0]?.role !== "launcher")
254
+ return null;
255
+ // What is executed is a bound file — the launcher itself, or the bound interpreter with the launcher as its script
256
+ // — and nothing a stored row could point elsewhere.
257
+ const launcher = bindings[0].path;
258
+ const prefix = value["prefix"];
259
+ if (value["file"] === launcher) {
260
+ if (prefix.length !== 0)
261
+ return null;
262
+ }
263
+ else {
264
+ const interpreter = bindings.find(one => one.role === "interpreter");
265
+ if (interpreter === undefined || interpreter.path !== value["file"] || prefix[prefix.length - 1] !== launcher)
266
+ return null;
267
+ }
268
+ return { file: value["file"], prefix, bindings };
269
+ }
@@ -0,0 +1,28 @@
1
+ import type { ProviderRunner } from "./provider.js";
2
+ import { type Store } from "./store.js";
3
+ import type { WorktreePool } from "./worktree.js";
4
+ export declare const CHECK_PROMPTS: {
5
+ readonly ok: "Create a file named done.txt in the current folder containing exactly the word ok. Then stop. Do nothing else.";
6
+ readonly stop: "Run the shell command `sleep 120` and wait for it to finish.";
7
+ };
8
+ export type AcpCheckJob = {
9
+ store: Store;
10
+ runner: string;
11
+ token: string;
12
+ /** The watch running it: its project and its lease's owner. */
13
+ watch: {
14
+ repo: string;
15
+ owner: string;
16
+ };
17
+ pool: WorktreePool;
18
+ clock?: () => Date;
19
+ keyHome?: string;
20
+ /** Tests only: a stand-in agent runner. */
21
+ agent?: ProviderRunner;
22
+ /** Tests only: a PATH to find the agent on. */
23
+ env?: Readonly<Record<string, string | undefined>>;
24
+ progress?: (line: string) => void;
25
+ };
26
+ /** Close what no live watch holds any more, let go of what finished checks still hold, then run this runner's next
27
+ * check, if one is waiting. */
28
+ export declare function runAcpChecks(job: AcpCheckJob): Promise<void>;