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
@@ -11,7 +11,7 @@
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import { spawn } from "node:child_process";
13
13
  import { existsSync } from "node:fs";
14
- import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
14
+ import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { dirname, join, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
@@ -82,16 +82,37 @@ function run(file, args, options = {}) {
82
82
  }
83
83
 
84
84
  /**
85
- * Build isolation sentinels: a hook in the repository's own and local Claude
86
- * settings and a server in its .mcp.json, each of which would leave a file
87
- * if it ran, beside one MCP server the project opts in to by name. A build
88
- * must start only the named one. They write outside the disposable state
89
- * folder, which the agent fence covers.
85
+ * Build isolation sentinels. Each fires on its own, with no help from the
86
+ * model, and appends "<launch> <sentinel>" to fired.log, where <launch> is
87
+ * the CANARY_LAUNCH of the process tree that ran it:
88
+ *
89
+ * - project-hook and local-hook: hooks in the repository's own and local
90
+ * Claude settings; repository-mcp: a server in its .mcp.json (the local
91
+ * settings enable every project server, so nothing waits on approval).
92
+ * - plugin: a Claude plugin named in CLAUDE_CODE_PLUGIN_DIRS, which the
93
+ * canary puts in every launch's environment; its hooks run whatever a
94
+ * launch's setting sources are.
95
+ * - adjacent-folder: the project's opted-in server reads a file in a
96
+ * repository beside the project's own as it starts. The agent fence keeps
97
+ * a build's tools inside its task boundary.
98
+ * - named-tool: that opted-in server started at all.
99
+ *
100
+ * The control launches the provider CLI unisolated (the repository's own
101
+ * setup loaded, the tool started directly, no fence), fresh and resumed:
102
+ * every sentinel must fire. The isolated launches are the real build and a
103
+ * resumed turn through the same gateway: only named-tool may fire. Claude
104
+ * only; other providers keep the repository checks without a control.
90
105
  */
91
- const MAKE = file => `node -e "require('fs').writeFileSync(process.argv[1],'ran')" ${JSON.stringify(file)}`;
92
- const OPT_IN_SERVER = `import { writeFileSync } from "node:fs";
106
+ const LOG = (log, kind) => `node -e "require('fs').appendFileSync(process.argv[1],(process.env.CANARY_LAUNCH||'unknown')+' '+process.argv[2]+'\\n')" ${JSON.stringify(log)} ${kind}`;
107
+ /** The opted-in tool's server. A brokered server gets only an allowlisted environment (tool-broker.ts), so it reads
108
+ * which launch it serves from the launch file the canary keeps beside the log; the control passes CANARY_LAUNCH. */
109
+ export const OPT_IN_SERVER = `import { appendFileSync, readFileSync } from "node:fs";
93
110
  import { createInterface } from "node:readline";
94
- writeFileSync(process.argv[2], "ran");
111
+ const [log, adjacent, launchFile] = process.argv.slice(2);
112
+ const launch = () => { try { return readFileSync(launchFile, "utf8").trim(); } catch { return ""; } };
113
+ const fired = kind => appendFileSync(log, (process.env.CANARY_LAUNCH || launch() || "unknown") + " " + kind + "\\n");
114
+ fired("named-tool");
115
+ try { readFileSync(adjacent); fired("adjacent-folder"); } catch { /* fenced */ }
95
116
  const send = message => process.stdout.write(JSON.stringify(message) + "\\n");
96
117
  createInterface({ input: process.stdin }).on("line", line => {
97
118
  const message = JSON.parse(line);
@@ -100,35 +121,132 @@ createInterface({ input: process.stdin }).on("line", line => {
100
121
  else if (message.id !== undefined) send({ jsonrpc: "2.0", id: message.id, result: {} });
101
122
  });
102
123
  `;
124
+ const SENTINELS = ["project-hook", "local-hook", "repository-mcp", "plugin", "adjacent-folder", "named-tool"];
103
125
 
104
- async function plantIsolationSentinels(repo, sentinels) {
105
- const hook = file => ({ hooks: [{ type: "command", command: MAKE(file) }] });
126
+ async function plantIsolationSentinels(base, repo, sentinels) {
127
+ const log = join(sentinels, "fired.log");
128
+ const hook = kind => ({ hooks: [{ type: "command", command: LOG(log, kind) }] });
106
129
  await mkdir(join(repo, ".claude"), { recursive: true });
107
- await writeFile(join(repo, ".claude", "settings.json"), `${JSON.stringify({ hooks: { SessionStart: [hook(join(sentinels, "project-hook"))], UserPromptSubmit: [hook(join(sentinels, "project-hook"))] } }, null, 2)}\n`);
108
- await writeFile(join(repo, ".claude", "settings.local.json"), `${JSON.stringify({ hooks: { UserPromptSubmit: [hook(join(sentinels, "local-hook"))] } }, null, 2)}\n`);
109
- await writeFile(join(repo, ".mcp.json"), `${JSON.stringify({ mcpServers: { "repo-sentinel": { type: "stdio", command: "node", args: ["-e", `require('fs').writeFileSync(${JSON.stringify(join(sentinels, "project-mcp"))},'ran')`] } } }, null, 2)}\n`);
130
+ await writeFile(join(repo, ".claude", "settings.json"), `${JSON.stringify({ hooks: { SessionStart: [hook("project-hook")], UserPromptSubmit: [hook("project-hook")] } }, null, 2)}\n`);
131
+ await writeFile(join(repo, ".claude", "settings.local.json"), `${JSON.stringify({ enableAllProjectMcpServers: true, hooks: { UserPromptSubmit: [hook("local-hook")] } }, null, 2)}\n`);
132
+ await writeFile(join(repo, ".mcp.json"), `${JSON.stringify({ mcpServers: { "repo-sentinel": { type: "stdio", command: "node", args: ["-e", `require('fs').appendFileSync(${JSON.stringify(log)},(process.env.CANARY_LAUNCH||'unknown')+' repository-mcp\\n')`] } } }, null, 2)}\n`);
133
+ await mkdir(join(sentinels, "plugin", ".claude-plugin"), { recursive: true });
134
+ await mkdir(join(sentinels, "plugin", "hooks"), { recursive: true });
135
+ await writeFile(join(sentinels, "plugin", ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "toolroll-canary-sentinel", version: "1.0.0", description: "Toolroll provider canary" })}\n`);
136
+ await writeFile(join(sentinels, "plugin", "hooks", "hooks.json"), `${JSON.stringify({ hooks: { SessionStart: [hook("plugin")], UserPromptSubmit: [hook("plugin")] } })}\n`);
110
137
  await writeFile(join(sentinels, "opt-in-server.mjs"), OPT_IN_SERVER);
138
+ // A repository beside the project's own, inside the build's task boundary.
139
+ await mkdir(join(base, "adjacent", ".git"), { recursive: true });
140
+ await writeFile(join(base, "adjacent", "secret.txt"), "adjacent\n");
141
+ await writeFile(launchFile(sentinels), "");
142
+ return { log, plugin: join(sentinels, "plugin"), server: [join(sentinels, "opt-in-server.mjs"), log, join(base, "adjacent", "secret.txt"), launchFile(sentinels)] };
143
+ }
144
+
145
+ /** Where the canary says which launch is running, for the brokered tool server that can't see CANARY_LAUNCH. */
146
+ const launchFile = sentinels => join(sentinels, "launch");
147
+ /** Name the launch about to run (null: none), in the environment its own processes inherit and in the launch file. */
148
+ async function tagLaunch(sentinels, launch) {
149
+ if (launch === null) delete process.env.CANARY_LAUNCH;
150
+ else process.env.CANARY_LAUNCH = launch;
151
+ await writeFile(launchFile(sentinels), launch ?? "");
152
+ }
153
+
154
+ /** The task the canary files: no quote marks, which a model may copy into the file. */
155
+ export function canaryGoal(provider, marker) {
156
+ return marker === null
157
+ ? `Add canary.txt: exactly standing-orders ${provider} canary passed plus newline. Add canary.test.js using node:test. Only these files; no questions; acceptance evidence: check and changed-path only.`
158
+ : `Add canary.txt: line 1 exactly standing-orders ${provider} canary passed; line 2 the TOOLROLL_MEMORY_CANARY marker memory gave you (never search), else NO_MEMORY. Add canary.test.js (node:test).`;
111
159
  }
112
160
 
113
161
  /** The project's one named tool, added the way the console adds it, before approval seals the task's tools. */
114
- async function optInNamedTool(db, repo, sentinels) {
162
+ async function optInNamedTool(db, repo, server) {
115
163
  const { openStore } = await import(join(root, "dist", "store.js"));
116
164
  const { addToolTo } = await import(join(root, "dist", "project-tools.js"));
117
165
  const store = openStore(db);
118
166
  try {
119
- const added = addToolTo(store, repo, { name: "canary-notes", transport: "stdio", command: process.execPath, args: [join(sentinels, "opt-in-server.mjs"), join(sentinels, "opt-in-mcp")], url: null, secrets: [], bearer: null, headerSecrets: {}, about: "The canary's opt-in server" }, "provider canary", "canary", new Date());
167
+ const added = addToolTo(store, repo, { name: "canary-notes", transport: "stdio", command: process.execPath, args: server, url: null, secrets: [], bearer: null, headerSecrets: {}, about: "The canary's opt-in server" }, "provider canary", "canary", new Date());
120
168
  if (!added.ok) throw new Error(`the canary's named tool was refused: ${added.message}`);
121
169
  } finally {
122
170
  store.close();
123
171
  }
124
172
  }
125
173
 
126
- function isolationOf(provider, sentinels) {
127
- const ran = name => existsSync(join(sentinels, name));
128
- const result = { projectHook: ran("project-hook"), localHook: ran("local-hook"), repositoryMcp: ran("project-mcp"), namedTool: ran("opt-in-mcp") };
129
- if (result.projectHook || result.localHook || result.repositoryMcp) throw new Error(`the build loaded the repository's own Claude setup: ${JSON.stringify(result)}`);
130
- if ((provider === "claude" || provider === "codex") && !result.namedTool) throw new Error("the project's named tool did not start for its build");
131
- return result;
174
+ /** Which sentinels each launch fired. */
175
+ export async function firedBy(log) {
176
+ const fired = {};
177
+ const text = existsSync(log) ? await readFile(log, "utf8") : "";
178
+ for (const line of text.split("\n").filter(Boolean)) {
179
+ const [launch, kind] = line.split(" ");
180
+ (fired[launch] ??= new Set()).add(kind);
181
+ }
182
+ return Object.fromEntries(Object.entries(fired).map(([launch, kinds]) => [launch, [...kinds].sort()]));
183
+ }
184
+
185
+ /**
186
+ * Every control launch fired every sentinel; every isolated launch fired
187
+ * only its named tool (Claude and Codex start it; the others have none).
188
+ */
189
+ export function judgeIsolation(provider, fired, controls, isolated, fence = null) {
190
+ const problems = [];
191
+ for (const launch of controls) {
192
+ const missing = SENTINELS.filter(kind => !(fired[launch] ?? []).includes(kind));
193
+ if (missing.length > 0) problems.push(`the unisolated ${launch} launch did not fire ${missing.join(", ")}, so the canary can't tell isolation from a sentinel that never runs`);
194
+ }
195
+ for (const launch of isolated) {
196
+ const leaked = (fired[launch] ?? []).filter(kind => kind !== "named-tool");
197
+ if (leaked.length > 0) problems.push(`the isolated ${launch} launch fired ${leaked.join(", ")}${fence !== null && leaked.includes("adjacent-folder") ? ` (its fence: ${fence})` : ""}`);
198
+ if ((provider === "claude" || provider === "codex") && !(fired[launch] ?? []).includes("named-tool")) problems.push(`the project's named tool did not start for the ${launch} launch`);
199
+ }
200
+ const unknown = Object.keys(fired).filter(launch => !controls.includes(launch) && !isolated.includes(launch));
201
+ if (unknown.length > 0) problems.push(`sentinels fired from an unexpected launch: ${unknown.map(launch => `${launch} (${fired[launch].join(", ")})`).join("; ")}`);
202
+ if (problems.length > 0) throw new Error(`Build isolation: ${problems.join("; ")}.`);
203
+ return Object.fromEntries([...controls, ...isolated].map(launch => [launch, fired[launch] ?? []]));
204
+ }
205
+
206
+ /** The unisolated control: the CLI as a build ran before isolation, in the repository, fresh and then resumed. */
207
+ async function claudeControl(repo, model, sentinels, server) {
208
+ const config = join(sentinels, "control-mcp.json");
209
+ await writeFile(config, JSON.stringify({ mcpServers: { "canary-notes": { type: "stdio", command: process.execPath, args: server } } }));
210
+ const turn = async (launch, resume) => {
211
+ const answer = await run("claude", ["-p", "Reply with exactly: control", "--model", model, "--max-turns", "1", "--output-format", "json",
212
+ "--setting-sources", "project,local", "--mcp-config", config, ...(resume === null ? [] : ["--resume", resume])], { cwd: repo, env: { ...process.env, CANARY_LAUNCH: launch } });
213
+ const envelope = parseEnvelope(answer, `the unisolated control (${launch})`);
214
+ if (typeof envelope.session_id !== "string") throw new Error(`the unisolated control (${launch}) named no session`);
215
+ return envelope.session_id;
216
+ };
217
+ const session = await turn("control-fresh", null);
218
+ await turn("control-resumed", session);
219
+ }
220
+
221
+ /** A resumed turn of the finished build through the real gateway: the same fence, broker and isolation a repair gets. */
222
+ async function resumeThroughGateway({ db, repo, provider, model, runnerToken, buildRunId, sentinels }) {
223
+ const { openStore } = await import(join(root, "dist", "store.js"));
224
+ const { acquire } = await import(join(root, "dist", "claim.js"));
225
+ const { invokeAgent } = await import(join(root, "dist", "invoke.js"));
226
+ const store = openStore(db);
227
+ try {
228
+ const build = store.getRun(buildRunId);
229
+ if (build?.sessionId == null || build.worktree == null) throw new Error("the build run kept no session or worktree to resume");
230
+ // Claude finds a session by the folder it ran in; a released worktree's path is made again, empty.
231
+ await mkdir(build.worktree, { recursive: true });
232
+ const now = new Date();
233
+ store.createTask({ id: `${TASK_ID}-resume`, title: "Resume the canary build once" }, now);
234
+ const ref = store.refFor("built-in", `${TASK_ID}-resume`).id;
235
+ store.placeTask(ref, repo);
236
+ const leaseId = `canary-resume-${randomUUID()}`;
237
+ const took = acquire(store, ref, "canary-worker", { now, token: runnerToken, newLeaseId: () => leaseId, ttlMs: 30 * 60_000 });
238
+ if (!took.ok) throw new Error(`the resumed turn could not claim its task: ${took.reason}`);
239
+ const id = store.startRun({ taskRef: ref, leaseId, runner: "canary-worker", branch: build.branch ?? "canary", worktree: build.worktree, route: { routeDigest: "legacy", phase: "build", provider, model, chosen: "legacy" }, now });
240
+ store.stampRun(id, { sessionId: build.sessionId });
241
+ await tagLaunch(sentinels, "isolated-resumed");
242
+ const result = await invokeAgent(store, id, { provider, model }, { phase: "repair", brief: "Reply with exactly: resumed. Do not change any file.", maxTurns: 2, permissionMode: "plan", skipPermissions: false, resumeSession: build.sessionId },
243
+ { cwd: build.worktree, env: { CANARY_LAUNCH: "isolated-resumed" }, timeoutMs: 600_000 }).finally(() => tagLaunch(sentinels, null));
244
+ store.finishRun(id, { outcome: result.kind === "ran" && result.outcome.code === 0 ? "built" : "failed", now: new Date() });
245
+ if (result.kind !== "ran" || result.outcome.code !== 0) throw new Error(`the resumed turn did not run: ${result.kind === "ran" ? `exit ${result.outcome.code}` : `${result.reason}: ${result.diagnostic ?? ""}`}`);
246
+ return { run: id, fence: store.runFence(id) };
247
+ } finally {
248
+ store.close();
249
+ }
132
250
  }
133
251
 
134
252
  /** Own only this disposable repository's memory directory. Never overwrite a
@@ -191,6 +309,45 @@ async function runtimeIdentity() {
191
309
  trackedChanges: changes.code === 0 ? changes.stdout.trim() !== "" : null };
192
310
  }
193
311
 
312
+ /** Verify Git from the produced checkout through the provider's generated policy, without a model turn. */
313
+ export async function checkCheckoutGit({ provider, worktree, databaseFile, repository, home = homedir() }, execute = run) {
314
+ if (!["claude", "codex", "gemini"].includes(provider)) return null;
315
+ if (typeof worktree !== "string" || worktree === "") throw new Error("the builder did not retain its worktree for the Git fence check");
316
+ const { agentFence, taskBoundary, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, fenceGrants, codexFenceArgv, macosFenceAvailable, macosFenced, linuxFenceAvailable, linuxFenced } = await import(join(root, "dist", "agent-fence.js"));
317
+ const binding = trustedWorktreeBinding(repository, worktree);
318
+ const boundary = taskBoundary({ worktree, provider, home, binding });
319
+ const fence = agentFence({ databaseFile, worktree, home, boundary, binding });
320
+ const grants = fenceGrants({ databaseFile, worktree, boundary });
321
+ const codexHome = provider === "codex" ? await mkdtemp(join(tmpdir(), "so-codex-canary-")) : null;
322
+ try {
323
+ let head = null;
324
+ let skipped = false;
325
+ const env = checkoutGitEnv({ ...process.env, ...(codexHome === null ? {} : { CODEX_HOME: codexHome }) });
326
+ for (const args of [["rev-parse", "HEAD"], ["status", "--porcelain"]]) {
327
+ const gitArgs = [...HARDENED_GIT_ARGS, ...args];
328
+ const control = await execute("git", gitArgs, { cwd: worktree, env });
329
+ if (control.code !== 0) throw new Error(`unfenced git ${args.join(" ")} failed: ${control.stderr}`);
330
+ const wrapped = provider === "codex"
331
+ ? { file: "codex", args: ["sandbox", ...codexFenceArgv(fence, false, grants), "-P", "standing-orders", "--", "git", ...gitArgs] }
332
+ : macosFenceAvailable() ? macosFenced("git", gitArgs, fence, grants)
333
+ : linuxFenceAvailable() ? linuxFenced("git", gitArgs, fence, grants) : null;
334
+ if (wrapped === null || fence.length === 0) { skipped = true; if (args[0] === "rev-parse") head = control.stdout.trim(); continue; }
335
+ const checked = await execute(wrapped.file, wrapped.args, { cwd: worktree,
336
+ env });
337
+ if (checked.code !== 0) {
338
+ const trivial = provider === "codex"
339
+ ? { file: "codex", args: ["sandbox", ...codexFenceArgv(fence, false, grants), "-P", "standing-orders", "--", "true"] }
340
+ : macosFenceAvailable() ? macosFenced("true", [], fence, grants) : linuxFenced("true", [], fence, grants);
341
+ const sandbox = await execute(trivial.file, trivial.args, { cwd: worktree, env });
342
+ throw new Error(sandbox.code === 0 ? `The build fence blocks this checkout's own Git data. git ${args.join(" ")}: ${checked.stderr}` : `The provider sandbox could not run its control: ${sandbox.stderr}`);
343
+ }
344
+ if (checked.stdout !== control.stdout) throw new Error(`fenced git ${args.join(" ")} disagrees with its control`);
345
+ if (args[0] === "rev-parse") head = checked.stdout.trim();
346
+ }
347
+ return { head, revParse: skipped ? "unfenced-only" : "passed", status: skipped ? "unfenced-only" : "passed", provider };
348
+ } finally { if (codexHome !== null) await rm(codexHome, { recursive: true, force: true }); }
349
+ }
350
+
194
351
  async function main() {
195
352
  let options;
196
353
  try {
@@ -253,10 +410,13 @@ async function main() {
253
410
  `${JSON.stringify({ name: "standing-orders-provider-canary", private: true, type: "module", scripts: { test: "node --test" } }, null, 2)}\n`,
254
411
  );
255
412
  await writeFile(join(repo, "README.md"), "# Toolroll provider canary\n\nThis repository is disposable.\n");
256
- await plantIsolationSentinels(repo, sentinels);
413
+ const planted = await plantIsolationSentinels(base, repo, sentinels);
414
+ // Every launch from here on (the CLI's planner and builder included) inherits the plugin sentinel.
415
+ process.env.CLAUDE_CODE_PLUGIN_DIRS = planted.plugin;
257
416
  await git("add", ".");
258
417
  await git("commit", "-qm", "seed provider canary");
259
418
  const baseSha = await git("rev-parse", "HEAD");
419
+ await symlink(repo, join(base, "repo-alias"), "dir");
260
420
 
261
421
  const checkBuild = async memoryMarker => {
262
422
 
@@ -284,13 +444,9 @@ async function main() {
284
444
  "--as", "canary", "--token", password,
285
445
  ]);
286
446
 
287
- await optInNamedTool(db, repo, sentinels);
288
- const title = [
289
- `Add canary.txt: ${memoryMarker === null ? "exactly" : "first line exactly"} "standing-orders ${options.provider} canary passed" plus newline.`,
290
- ...(memoryMarker === null ? [] : ["On its second line, report the TOOLROLL_MEMORY_CANARY marker only if automatic memory already supplied one; otherwise write NO_MEMORY. Do not read home files or search for memory. End that line with a newline."]),
291
- "Add canary.test.js using node:test.",
292
- "Only these files; no questions; acceptance evidence: check and changed-path only.",
293
- ].join(" ");
447
+ await optInNamedTool(db, repo, planted.server);
448
+ // A title is at most 200 characters, so the memory check says only what it must.
449
+ const title = canaryGoal(options.provider, memoryMarker);
294
450
  await cli("file the task", ["task", "add", title, "--id", TASK_ID, "--repo", repo]);
295
451
  let filedDigest = null;
296
452
  if (options.autoApprove) {
@@ -313,6 +469,7 @@ async function main() {
313
469
  "--as", "canary", "--token", password,
314
470
  ]);
315
471
  if (options.autoApprove && requested.autoPlan !== true) throw new Error("the exact filed plan was not pre-authorized");
472
+ await tagLaunch(sentinels, "isolated-plan");
316
473
  const planned = await cli("run the planning pass", [
317
474
  "tick", "--runner", "canary-worker", "--token", runnerToken,
318
475
  "--repo", repo, "--pool", pool,
@@ -336,10 +493,13 @@ async function main() {
336
493
  "--as", "canary", "--token", password,
337
494
  ]);
338
495
 
496
+ await tagLaunch(sentinels, "isolated-fresh");
339
497
  const built = await cli("run the build and evidence pass", [
340
498
  "tick", "--runner", "canary-worker", "--token", runnerToken,
341
499
  "--repo", repo, "--pool", pool,
342
500
  ]);
501
+ // Anything launched after the build is no isolated launch of this canary's: it would fire as "unknown".
502
+ await tagLaunch(sentinels, null);
343
503
  if (!Array.isArray(built.dispatched) || !built.dispatched.some(item => item?.outcome === "built")) {
344
504
  throw new Error(`builder did not complete the task: ${JSON.stringify(built)}`);
345
505
  }
@@ -352,7 +512,9 @@ async function main() {
352
512
  if (buildRun === null || buildRun === undefined || typeof buildRun.branch !== "string") {
353
513
  throw new Error("no completed builder run named its branch");
354
514
  }
515
+ const checkoutGit = await checkCheckoutGit({ provider: options.provider, worktree: buildRun.worktree, databaseFile: db, repository: repo });
355
516
  const branchSha = await git("rev-parse", buildRun.branch);
517
+ if (checkoutGit !== null && checkoutGit.head !== branchSha) throw new Error("the fenced checkout is not the completed build candidate");
356
518
  if (branchSha === baseSha) throw new Error("the build branch did not advance");
357
519
  const canaryText = await git("show", `${buildRun.branch}:canary.txt`);
358
520
  const expectedText = `standing-orders ${options.provider} canary passed${memoryMarker === null ? "" : "\nNO_MEMORY"}`;
@@ -376,7 +538,23 @@ async function main() {
376
538
  if (await git("rev-parse", "main") !== baseSha) throw new Error("the default branch changed");
377
539
  if (Number(await git("rev-list", "--count", `${baseSha}..${branchSha}`)) !== 1) throw new Error("expected exactly one work commit");
378
540
  await assertRuntime();
379
- const isolation = isolationOf(options.provider, sentinels);
541
+ const controls = [];
542
+ const isolatedLaunches = ["isolated-plan", "isolated-fresh"];
543
+ let resumed = null;
544
+ if (options.provider === "claude") {
545
+ resumed = await resumeThroughGateway({ db, repo, provider: options.provider, model: options.model, runnerToken, buildRunId: buildRun.id, sentinels });
546
+ isolatedLaunches.push("isolated-resumed");
547
+ // The control names its launch in its own environment; the launch file stays empty.
548
+ await tagLaunch(sentinels, null);
549
+ await claudeControl(repo, options.model, sentinels, planted.server);
550
+ controls.push("control-fresh", "control-resumed");
551
+ }
552
+ const fence = await (async () => {
553
+ const { openStore } = await import(join(root, "dist", "store.js"));
554
+ const store = openStore(db);
555
+ try { return store.runFence(buildRun.id)?.method ?? null; } finally { store.close(); }
556
+ })();
557
+ const isolation = { fence, launches: judgeIsolation(options.provider, await firedBy(planted.log), controls, isolatedLaunches, fence), resumedRun: resumed };
380
558
  if (memoryMarker !== null) {
381
559
  if (JSON.stringify(final).includes(memoryMarker)) throw new Error("Claude supplied the disposable automatic-memory marker to the task.");
382
560
  isolation.autoMemory = "NO_MEMORY";
@@ -403,6 +581,7 @@ async function main() {
403
581
  humanActionsAfterPlanningStarted: options.autoApprove ? 0 : 1,
404
582
  filedScopeDigest: filedDigest,
405
583
  worktreeBuild: "passed",
584
+ checkoutGit,
406
585
  commit: branchSha,
407
586
  verificationCommand: "node --test",
408
587
  proofVerdict: final.proofVerdict,