toolroll 0.9.63 → 0.9.67

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 (302) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +33 -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 +117 -2
  16. package/dist/agent-fence.js +439 -12
  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 +39 -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/batch-checks.js +3 -2
  32. package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
  33. package/dist/browser/terminal.css +37 -0
  34. package/dist/browser/terminal.js +228 -0
  35. package/dist/browser/workspace.css +2 -2
  36. package/dist/browser/workspace.js +42 -38
  37. package/dist/browser-shell.js +6 -3
  38. package/dist/browser-workspace.d.ts +235 -1
  39. package/dist/browser-workspace.js +60 -0
  40. package/dist/builder.d.ts +10 -1
  41. package/dist/builder.js +87 -24
  42. package/dist/chat-actions.d.ts +13 -1
  43. package/dist/chat-actions.js +197 -15
  44. package/dist/chat-ask.d.ts +4 -0
  45. package/dist/chat-ask.js +14 -0
  46. package/dist/chat-channel.js +20 -0
  47. package/dist/chat-continuity.js +5 -4
  48. package/dist/chat-decide.d.ts +0 -14
  49. package/dist/chat-decide.js +4 -1
  50. package/dist/chat-delivery.d.ts +9 -0
  51. package/dist/chat-delivery.js +45 -6
  52. package/dist/chat-messages.d.ts +8 -0
  53. package/dist/chat-messages.js +36 -4
  54. package/dist/check-levels-ui.js +2 -2
  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 +19 -0
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +4 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +21 -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/console-api.d.ts +13 -4
  71. package/dist/contracts/console-api.js +13 -4
  72. package/dist/contracts/flow-card.d.ts +4 -0
  73. package/dist/contracts/flow-card.js +3 -0
  74. package/dist/contracts/flow.d.ts +46 -0
  75. package/dist/contracts/flow.js +11 -2
  76. package/dist/contracts/lead-tools.d.ts +92 -0
  77. package/dist/contracts/lead-tools.js +13 -0
  78. package/dist/contracts/provider.d.ts +11 -1
  79. package/dist/contracts/provider.js +1 -1
  80. package/dist/contracts/route.d.ts +76 -1
  81. package/dist/contracts/route.js +1 -1
  82. package/dist/contracts/store-json.d.ts +2 -0
  83. package/dist/control-setup.d.ts +1 -1
  84. package/dist/control-ui.js +2 -2
  85. package/dist/converse.d.ts +16 -0
  86. package/dist/converse.js +32 -4
  87. package/dist/demo-chat.js +4 -4
  88. package/dist/demo.js +2 -2
  89. package/dist/desktop-update-gate.js +6 -0
  90. package/dist/desktop-update.d.ts +1 -1
  91. package/dist/discord-chat.js +4 -1
  92. package/dist/doctor.js +1 -1
  93. package/dist/egress-bridge.d.ts +17 -0
  94. package/dist/egress-bridge.js +111 -0
  95. package/dist/egress.d.ts +119 -0
  96. package/dist/egress.js +423 -0
  97. package/dist/exec.d.ts +78 -0
  98. package/dist/exec.js +367 -9
  99. package/dist/export-ui.js +2 -2
  100. package/dist/flow-engine.js +24 -5
  101. package/dist/flow-gallery-ui.js +2 -2
  102. package/dist/flow-gallery.js +27 -8
  103. package/dist/flow-share.js +16 -5
  104. package/dist/flow-triggers.d.ts +77 -8
  105. package/dist/flow-triggers.js +517 -70
  106. package/dist/flows-cli.js +8 -2
  107. package/dist/flows-ui.d.ts +0 -2
  108. package/dist/flows-ui.js +52 -4
  109. package/dist/flows.d.ts +4 -0
  110. package/dist/flows.js +9 -1
  111. package/dist/html.d.ts +9 -0
  112. package/dist/html.js +11 -0
  113. package/dist/invoke.d.ts +35 -0
  114. package/dist/invoke.js +347 -28
  115. package/dist/keys.d.ts +18 -14
  116. package/dist/keys.js +5 -0
  117. package/dist/lead-capabilities.js +3 -2
  118. package/dist/lead-contract.js +2 -0
  119. package/dist/lead-doors.d.ts +2 -0
  120. package/dist/lead-doors.js +7 -0
  121. package/dist/lead-follow.d.ts +2 -0
  122. package/dist/lead-follow.js +58 -2
  123. package/dist/lead-progress.js +1 -0
  124. package/dist/lead-queue-runtime.d.ts +46 -0
  125. package/dist/lead-queue-runtime.js +191 -0
  126. package/dist/lead-queue.d.ts +104 -0
  127. package/dist/lead-queue.js +187 -0
  128. package/dist/lead-sweep.d.ts +67 -0
  129. package/dist/lead-sweep.js +99 -0
  130. package/dist/lead-tools.d.ts +22 -0
  131. package/dist/lead-tools.js +152 -1
  132. package/dist/lead.d.ts +13 -5
  133. package/dist/lead.js +89 -29
  134. package/dist/limits-ui.d.ts +8 -1
  135. package/dist/limits-ui.js +26 -0
  136. package/dist/live-bus.d.ts +8 -0
  137. package/dist/live-bus.js +40 -4
  138. package/dist/live-detail.d.ts +27 -0
  139. package/dist/live-detail.js +57 -0
  140. package/dist/live-notes.d.ts +20 -0
  141. package/dist/live-notes.js +47 -0
  142. package/dist/live-records.d.ts +140 -0
  143. package/dist/live-records.js +935 -0
  144. package/dist/live-terminal.d.ts +269 -0
  145. package/dist/live-terminal.js +686 -0
  146. package/dist/live.d.ts +38 -27
  147. package/dist/live.js +142 -124
  148. package/dist/mcp-oauth.d.ts +6 -3
  149. package/dist/mcp-oauth.js +8 -4
  150. package/dist/memory-pass.js +2 -2
  151. package/dist/mobile-viewport.d.ts +5 -1
  152. package/dist/mobile-viewport.js +18 -2
  153. package/dist/model-catalog.js +4 -0
  154. package/dist/monitoring-ui.js +2 -2
  155. package/dist/needs-you.d.ts +1 -1
  156. package/dist/needs-you.js +11 -4
  157. package/dist/operate.d.ts +4 -0
  158. package/dist/operate.js +335 -19
  159. package/dist/passwordless.d.ts +23 -0
  160. package/dist/passwordless.js +26 -0
  161. package/dist/peek-cli.d.ts +12 -4
  162. package/dist/peek-cli.js +19 -11
  163. package/dist/phase-routing.js +10 -3
  164. package/dist/planner-source.d.ts +3 -0
  165. package/dist/planner-source.js +14 -0
  166. package/dist/planner.d.ts +2 -1
  167. package/dist/planner.js +7 -1
  168. package/dist/policy-ui.js +2 -2
  169. package/dist/policy.d.ts +1 -20
  170. package/dist/policy.js +29 -2
  171. package/dist/project-delete-ui.d.ts +18 -0
  172. package/dist/project-delete-ui.js +60 -9
  173. package/dist/project-delete.js +2 -2
  174. package/dist/project-memory.d.ts +3 -1
  175. package/dist/project-memory.js +57 -25
  176. package/dist/project-tools.d.ts +5 -3
  177. package/dist/project-tools.js +12 -9
  178. package/dist/provider-auth.d.ts +4 -3
  179. package/dist/provider-auth.js +5 -4
  180. package/dist/provider-connection.d.ts +2 -1
  181. package/dist/provider-connection.js +5 -0
  182. package/dist/provider.d.ts +25 -2
  183. package/dist/provider.js +63 -9
  184. package/dist/pull-request-ui.js +2 -2
  185. package/dist/push.js +1 -1
  186. package/dist/quick-start.d.ts +24 -0
  187. package/dist/quick-start.js +127 -0
  188. package/dist/release-adapters.d.ts +6 -0
  189. package/dist/release-adapters.js +70 -3
  190. package/dist/release.d.ts +10 -1
  191. package/dist/release.js +34 -15
  192. package/dist/request-budget-ui.js +2 -2
  193. package/dist/result-follow-ups.js +3 -2
  194. package/dist/result-review.d.ts +1 -1
  195. package/dist/result-review.js +9 -3
  196. package/dist/retention-ui.js +2 -2
  197. package/dist/retention.js +10 -1
  198. package/dist/run-efficiency.d.ts +73 -0
  199. package/dist/run-efficiency.js +228 -0
  200. package/dist/runner.d.ts +5 -1
  201. package/dist/runner.js +22 -1
  202. package/dist/scope.d.ts +183 -5
  203. package/dist/scope.js +256 -31
  204. package/dist/scout-net.d.ts +4 -1
  205. package/dist/scout-net.js +37 -12
  206. package/dist/scout.d.ts +20 -6
  207. package/dist/scout.js +27 -3
  208. package/dist/serve.js +248 -40
  209. package/dist/server/chat.d.ts +1 -1
  210. package/dist/server/chat.js +122 -31
  211. package/dist/server/chrome.d.ts +16 -2
  212. package/dist/server/chrome.js +63 -11
  213. package/dist/server/console.css +107 -42
  214. package/dist/server/flows.js +38 -8
  215. package/dist/server/guards.d.ts +1 -0
  216. package/dist/server/guards.js +23 -9
  217. package/dist/server/http.d.ts +7 -0
  218. package/dist/server/http.js +3 -2
  219. package/dist/server/live.d.ts +34 -1
  220. package/dist/server/live.js +39 -17
  221. package/dist/server/pages-assets.js +5 -0
  222. package/dist/server/pages.js +90 -13
  223. package/dist/server/people-tokens.d.ts +3 -0
  224. package/dist/server/people-tokens.js +55 -17
  225. package/dist/server/remote-hooks.js +12 -6
  226. package/dist/server/remote.d.ts +2 -1
  227. package/dist/server/remote.js +14 -5
  228. package/dist/server/render-chat.d.ts +12 -3
  229. package/dist/server/render-chat.js +47 -16
  230. package/dist/server/render-pages.d.ts +32 -4
  231. package/dist/server/render-pages.js +83 -13
  232. package/dist/server/render-people.d.ts +3 -2
  233. package/dist/server/render-people.js +3 -2
  234. package/dist/server/render-results.d.ts +18 -2
  235. package/dist/server/render-results.js +37 -28
  236. package/dist/server/render-settings.js +6 -5
  237. package/dist/server/render-tasks.d.ts +35 -10
  238. package/dist/server/render-tasks.js +111 -27
  239. package/dist/server/request-context.d.ts +2 -0
  240. package/dist/server/request-context.js +2 -1
  241. package/dist/server/route-table.d.ts +3 -0
  242. package/dist/server/route-table.js +47 -5
  243. package/dist/server/runtime.d.ts +33 -2
  244. package/dist/server/session.d.ts +25 -1
  245. package/dist/server/session.js +18 -4
  246. package/dist/server/settings.js +142 -10
  247. package/dist/server/sweeps.d.ts +4 -0
  248. package/dist/server/sweeps.js +58 -0
  249. package/dist/server/task-views.d.ts +2 -1
  250. package/dist/server/task-views.js +96 -6
  251. package/dist/server/tasks.js +327 -78
  252. package/dist/server/terminals.d.ts +31 -0
  253. package/dist/server/terminals.js +365 -0
  254. package/dist/setup-guide.js +3 -2
  255. package/dist/slack-chat.js +4 -1
  256. package/dist/spend-ui.js +3 -3
  257. package/dist/spend.js +2 -1
  258. package/dist/steer-capabilities.d.ts +32 -0
  259. package/dist/steer-capabilities.js +60 -0
  260. package/dist/steer-controls.d.ts +14 -0
  261. package/dist/steer-controls.js +36 -0
  262. package/dist/steer-status.js +2 -2
  263. package/dist/storage-ui.js +4 -4
  264. package/dist/store-baseline.d.ts +1 -1
  265. package/dist/store-baseline.js +1 -1
  266. package/dist/store.d.ts +440 -15
  267. package/dist/store.js +1072 -52
  268. package/dist/subagent-desk.js +1 -1
  269. package/dist/summary.js +3 -0
  270. package/dist/task-source.d.ts +58 -0
  271. package/dist/task-source.js +93 -0
  272. package/dist/team-leads.js +1 -1
  273. package/dist/teams-chat.js +4 -1
  274. package/dist/teardown-git.d.ts +22 -0
  275. package/dist/teardown-git.js +136 -2
  276. package/dist/telegram-lead.d.ts +3 -0
  277. package/dist/telegram-lead.js +44 -7
  278. package/dist/telegram.d.ts +11 -2
  279. package/dist/telegram.js +51 -17
  280. package/dist/terminal-catalog.d.ts +11 -0
  281. package/dist/terminal-catalog.js +129 -0
  282. package/dist/text-limits.d.ts +2 -0
  283. package/dist/text-limits.js +2 -0
  284. package/dist/tool-broker.d.ts +14 -0
  285. package/dist/tool-broker.js +67 -2
  286. package/dist/toolroll-update-ui.js +2 -2
  287. package/dist/toolroll-update.js +13 -3
  288. package/dist/tools-ui.js +2 -2
  289. package/dist/unfenced-test-support.d.ts +2 -0
  290. package/dist/unfenced-test-support.js +11 -0
  291. package/dist/webhook-limits.d.ts +23 -0
  292. package/dist/webhook-limits.js +30 -0
  293. package/dist/webhook-readers.d.ts +56 -0
  294. package/dist/webhook-readers.js +138 -0
  295. package/dist/webhook-signing.d.ts +27 -0
  296. package/dist/webhook-signing.js +119 -0
  297. package/dist/work-index.d.ts +29 -0
  298. package/dist/work-index.js +16 -2
  299. package/dist/workspace-revision.js +3 -1
  300. package/dist/writes-mode.d.ts +9 -0
  301. package/dist/writes-mode.js +8 -0
  302. package/package.json +15 -2
package/dist/converse.js CHANGED
@@ -393,6 +393,20 @@ export function leadWorstCaseForPrice(price, baseBytes, caps = {}) {
393
393
  export function embeddedBytes(text) {
394
394
  return Buffer.byteLength(JSON.stringify(text), "utf8");
395
395
  }
396
+ /** Anthropic prices a cache write at 1.25x and a cache read at 0.1x the input price (5-minute cache). */
397
+ export const CACHE_WRITE_RATIO = 1.25;
398
+ export const CACHE_READ_RATIO = 0.1;
399
+ /** The price a worst case reserves at while prompt caching is on: every input token as a cache write (the dearest). */
400
+ export function cacheWorstPrice(price) {
401
+ return { inMicrousd: Math.ceil(price.inMicrousd * CACHE_WRITE_RATIO), outMicrousd: price.outMicrousd };
402
+ }
403
+ /** A step's settlement with its cache categories: plain input, cache writes and cache reads each at their own rate. */
404
+ export function settleWithCache(price, answer) {
405
+ const write = answer.cacheWriteTokens ?? 0, read = answer.cacheReadTokens ?? 0;
406
+ const plain = answer.tokensIn - write - read;
407
+ return plain * price.inMicrousd + write * Math.ceil(price.inMicrousd * CACHE_WRITE_RATIO) + read * Math.ceil(price.inMicrousd * CACHE_READ_RATIO)
408
+ + answer.tokensOut * price.outMicrousd;
409
+ }
396
410
  /**
397
411
  * The tool-capable wrapper parser (mate arc). Same caps and the same
398
412
  * duplicate-key lexer as fleet chat's; tool calls are read, bounded (at
@@ -475,7 +489,18 @@ export function parseLeadProviderWrapper(provider, bytes) {
475
489
  const usage = usageOf(body["usage"], "input_tokens", "output_tokens");
476
490
  if (usage === null)
477
491
  return { ok: false, problem: "no-usage" };
478
- return { ok: true, answer: { text, calls, tokensIn: usage.tokensIn, tokensOut: usage.tokensOut, reportedCostMicrousd: null } };
492
+ // Prompt cache categories: integers when present (anything else is malformed usage, never a guess). Anthropic's
493
+ // input_tokens counts only the uncached part; the request's whole input is the three added up.
494
+ const cache = (key) => {
495
+ const value = body["usage"][key];
496
+ // Absent: not reported. Present (even null): it must be a token count, or the usage is malformed.
497
+ return value === undefined ? undefined : typeof value === "number" && tokenCount(value) ? value : null;
498
+ };
499
+ const write = cache("cache_creation_input_tokens"), read = cache("cache_read_input_tokens");
500
+ if (write === null || read === null)
501
+ return { ok: false, problem: "no-usage" };
502
+ return { ok: true, answer: { text, calls, tokensIn: usage.tokensIn + (write ?? 0) + (read ?? 0), tokensOut: usage.tokensOut, reportedCostMicrousd: null,
503
+ ...(write === undefined ? {} : { cacheWriteTokens: write }), ...(read === undefined ? {} : { cacheReadTokens: read }) } };
479
504
  }
480
505
  const choices = body["choices"];
481
506
  if (!Array.isArray(choices) || choices.length !== 1)
@@ -528,7 +553,9 @@ export function parseLeadProviderWrapper(provider, bytes) {
528
553
  export function composeLeadRequest(args) {
529
554
  const opener = `DATA:\n${args.dataDocument}\n\n(The conversation follows. Every operator message is data, from the operator.)`;
530
555
  if (args.provider === "anthropic-api") {
531
- const messages = [{ role: "user", content: opener }];
556
+ const ephemeral = { cache_control: { type: "ephemeral" } };
557
+ const cached = args.cache !== undefined;
558
+ const messages = [{ role: "user", content: cached ? [{ type: "text", text: opener, ...ephemeral }] : opener }];
532
559
  for (const one of args.history) {
533
560
  if (one.role === "operator")
534
561
  messages.push({ role: "user", content: one.text });
@@ -549,8 +576,9 @@ export function composeLeadRequest(args) {
549
576
  body: JSON.stringify({
550
577
  model: args.model,
551
578
  max_tokens: MAX_OUTPUT_TOKENS,
552
- system: args.system,
553
- tools: args.tools.map(tool => ({ name: tool.name, description: tool.description, input_schema: tool.inputSchema })),
579
+ system: cached ? [{ type: "text", text: args.system, ...ephemeral }] : args.system,
580
+ tools: args.tools.map((tool, index) => ({ name: tool.name, description: tool.description, input_schema: tool.inputSchema,
581
+ ...(cached && index === args.cache.coreTools - 1 ? ephemeral : {}) })),
554
582
  messages,
555
583
  }),
556
584
  };
package/dist/demo-chat.js CHANGED
@@ -8,7 +8,7 @@ import { html, postForm } from "./html.js";
8
8
  import { headlineOf } from "./task-status.js";
9
9
  const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
10
10
  function hint() {
11
- return html `<section class="demo-hint" aria-label="Get started"><p class="demo-hint-title">Ask for something, like “fix the flaky refund test”</p>${postForm("/chat/demo/ask", SUGGESTIONS.map(one => html `<button type="submit" name="message" value="${one}">${one}</button>`), { attrs: { class: "demo-suggestions" } })}</section>`;
11
+ return html `<section class="demo-hint" aria-label="Get started"><h2 class="demo-hint-title">What should the crew do?</h2><p class="demo-hint-sub">Ask for something, like “fix the flaky refund test”, or start from one of these.</p>${postForm("/chat/demo/ask", SUGGESTIONS.map(one => html `<button type="submit" name="message" value="${one}">${one}</button>`), { attrs: { class: "demo-suggestions" } })}</section>`;
12
12
  }
13
13
  function planCard(exchange) {
14
14
  const plan = exchange.plan;
@@ -55,7 +55,7 @@ export function demoThreadHtml(exchanges, resultOf) {
55
55
  return html `${exchanges.map(exchange => {
56
56
  const parts = [
57
57
  html `<div class="demo-said demo-you"><p>${exchange.asked}</p></div>`,
58
- html `<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>${exchange.reply}</p></div>`,
58
+ html `<div class="demo-said demo-lead"><p class="demo-who"><span class="demo-avatar" aria-hidden="true">L</span>Lead</p><p>${exchange.reply}</p></div>`,
59
59
  planCard(exchange),
60
60
  ];
61
61
  if (exchange.state === "working")
@@ -63,14 +63,14 @@ export function demoThreadHtml(exchanges, resultOf) {
63
63
  if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
64
64
  parts.push(resultCard(exchange, resultOf(exchange)));
65
65
  if (exchange.state === "complete") {
66
- parts.push(html `<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, html `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2><p>In your repository's folder, run:</p><pre class="demo-command"><code>${START_COMMAND}</code></pre><p class="meta">It opens in your browser, already signed in.</p></section>`);
66
+ parts.push(html `<div class="demo-said demo-lead"><p class="demo-who"><span class="demo-avatar" aria-hidden="true">L</span>Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, html `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2><p>In your repository's folder, run:</p><pre class="demo-command"><code>${START_COMMAND}</code></pre><p class="meta">It opens in your browser, already signed in.</p></section>`);
67
67
  }
68
68
  return html `<article class="demo-turn" id="demo-${exchange.id}">${parts}</article>`;
69
69
  })}`;
70
70
  }
71
71
  export function demoChatHtml(input) {
72
72
  const working = input.exchanges.some(one => one.state === "working");
73
- return html `<div class="demo-chat">${input.problem === null ? "" : html `<p class="problem" role="alert">${input.problem}</p>`}<div id="demo-thread" class="demo-thread" aria-live="polite" data-version="${input.version}" data-working="${working ? "1" : "0"}">${demoThreadHtml(input.exchanges, input.resultOf)}</div>${postForm("/chat/demo/ask", html `<label for="demo-message" class="so-sr-only">Message the lead</label><textarea id="demo-message" name="message" rows="2" maxlength="500" placeholder="Ask the lead for a change" required></textarea><button type="submit" class="demo-primary">Send</button>`, { attrs: { class: "demo-composer" } })}</div>`;
73
+ return html `<div class="demo-chat${input.exchanges.length === 0 ? " demo-chat--empty" : ""}">${input.problem === null ? "" : html `<p class="problem" role="alert">${input.problem}</p>`}<div id="demo-thread" class="demo-thread" aria-live="polite" data-version="${input.version}" data-working="${working ? "1" : "0"}">${demoThreadHtml(input.exchanges, input.resultOf)}</div>${postForm("/chat/demo/ask", html `<label for="demo-message" class="so-sr-only">Message the lead</label><div class="demo-composer-card"><textarea id="demo-message" name="message" rows="1" maxlength="500" placeholder="Ask the lead for a change" required aria-describedby="demo-composer-hint"></textarea><div class="demo-composer-bar"><span id="demo-composer-hint">Enter to send · Shift + Enter for a new line</span><button type="submit" class="demo-primary demo-send" aria-label="Send"><svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5"/></svg></button></div></div>`, { attrs: { class: "demo-composer" } })}</div>`;
74
74
  }
75
75
  /**
76
76
  * Keeps a running build live: polls while one is working and swaps the
package/dist/demo.js CHANGED
@@ -1044,14 +1044,14 @@ function seedDemoFlows(store, folder, now) {
1044
1044
  ["Parcel arrived crushed", "The box was crushed and two glasses inside are broken.", "Delivery problem", 0.91, "delivery", "Soon: the customer is waiting on it", true, 120],
1045
1045
  ["Where is my order and why was I billed?", "It hasn't arrived and there's a charge I don't recognise.", "Delivery problem", 0.58, "by-hand", "Soon: the customer is waiting on it", false, 45],
1046
1046
  ]) {
1047
- const card = store.addFlowCard({ flow: desk, title, description, stage: "sort", by: "Support form" }, at(minutes));
1047
+ const card = store.addFlowCard({ flow: desk, title, description, stage: "sort", by: "Support form", byKind: "automation" }, at(minutes));
1048
1048
  const confident = sure >= 0.8;
1049
1049
  step(card, "sort", "sort", at(minutes - 1), confident ? `${answer}, ${Math.round(sure * 100)}% sure.` : `Not sure: ${Math.round(sure * 100)}% ${answer}.`, `${confident ? `${answer}, ${Math.round(sure * 100)}% sure.` : `Not sure: ${Math.round(sure * 100)}% ${answer}.`} How urgent is it? ${urgency.split(":")[0]}.`, { decisionJson: sorted(answer, sure, to, urgency, refund) });
1050
1050
  move(card, to, at(minutes - 1));
1051
1051
  }
1052
1052
  const replies = store.createFlow({ repo, name: "Customer replies", definitionJson: template("email-replies"), by: "demo" }, at(200));
1053
1053
  const drafted = (title, description, draft, minutes) => {
1054
- const card = store.addFlowCard({ flow: replies, title, description, stage: "write", by: "Contact form" }, at(minutes));
1054
+ const card = store.addFlowCard({ flow: replies, title, description, stage: "write", by: "Contact form", byKind: "automation" }, at(minutes));
1055
1055
  step(card, "write", "draft", at(minutes - 1), `Drafted ${draft.split(/\s+/).length} words.`, draft, { log: `Claude (sonnet) · 4.2 s\n\n${draft}` });
1056
1056
  move(card, "check", at(minutes - 1));
1057
1057
  return card;
@@ -1,4 +1,5 @@
1
1
  import { activeCodingUpdateWork, installCodingUpdateGate, removeCodingUpdateGate } from "./coding-update.js";
2
+ import { activeTerminalUpdateWork, installTerminalUpdateGate, removeTerminalUpdateGate } from "./terminal-catalog.js";
2
3
  export const UPDATE_PAUSED = "Toolroll is updating. Current work can finish; new work will resume after the update. Open Update status in the desktop app.";
3
4
  const prefix = "so_desktop_update_";
4
5
  const rows = (db) => db.prepare("SELECT name, sql FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*' ORDER BY name").all();
@@ -36,6 +37,7 @@ export function installUpdateGate(db, id) {
36
37
  throw error;
37
38
  }
38
39
  installCodingUpdateGate(db, id);
40
+ installTerminalUpdateGate(db, id);
39
41
  }
40
42
  export function removeUpdateGate(db, id) {
41
43
  const wanted = [...statements(id), ...statements(id, true)];
@@ -45,6 +47,7 @@ export function removeUpdateGate(db, id) {
45
47
  if (existing.some(row => !owns(wanted, row.sql)))
46
48
  throw Error("This update does not own the admission pause. Nothing was cleared.");
47
49
  removeCodingUpdateGate(db, id);
50
+ removeTerminalUpdateGate(db, id);
48
51
  for (const row of existing)
49
52
  db.exec(`DROP TRIGGER "${String(row.name)}"`);
50
53
  db.exec("COMMIT");
@@ -67,6 +70,8 @@ export function activeUpdateWork(db) {
67
70
  conversations: count("SELECT count(*) n FROM mate_turn WHERE state IN ('queued','running')") + count("SELECT count(*) n FROM chat_turn WHERE state IN ('queued','running')"),
68
71
  stopping: count("SELECT count(*) n FROM run_stop WHERE settled_at IS NULL"),
69
72
  ...activeCodingUpdateWork(db),
73
+ // Live terminals in a console that is still running (terminal-catalog.ts).
74
+ ...activeTerminalUpdateWork(db),
70
75
  };
71
76
  }
72
77
  /** Last admission race closes in the same SQLite write transaction as the
@@ -77,6 +82,7 @@ export function freezeUpdateGate(db, id) {
77
82
  if (!updateGateOwned(db, id))
78
83
  throw Error("The update no longer owns admission.");
79
84
  installCodingUpdateGate(db, id);
85
+ installTerminalUpdateGate(db, id);
80
86
  const idle = Object.values(activeUpdateWork(db)).every(n => n === 0);
81
87
  if (idle) {
82
88
  for (const row of rows(db))
@@ -94,7 +94,6 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
94
94
  export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
95
95
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
96
96
  export declare function desktopUpdateStatus(stateDir: string): {
97
- version?: never;
98
97
  active: boolean;
99
98
  phase: string;
100
99
  detail: string;
@@ -108,6 +107,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
108
107
  wasRunning?: never;
109
108
  backupPath?: never;
110
109
  updateId?: never;
110
+ version?: never;
111
111
  buildId?: never;
112
112
  workDir?: never;
113
113
  } | {
@@ -8,7 +8,7 @@ import { roomCommand } from "./chat-rooms.js";
8
8
  import { ChatState, ChatDeliveryError, chatHash, partContent, } from "./chat-delivery-state.js";
9
9
  import { readChatPart } from "./contracts/chat-content.js";
10
10
  import { readDiscordButtonToken, readDiscordComponentInteraction } from "./contracts/discord-callback.js";
11
- import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, } from "./chat-delivery.js";
11
+ import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
12
12
  import { chatResultHref } from "./chat-controls.js";
13
13
  import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
14
14
  import { discordPlain, renderReply } from "./reply-shape.js";
@@ -379,6 +379,9 @@ export async function deliverDiscordPart(options) {
379
379
  fail_if_not_exists: false,
380
380
  };
381
381
  }
382
+ // The watch, at the last moment before the network (cockpit §3).
383
+ if (heldForWatch(state, store, row.id, event, binding.approver, content, now))
384
+ return true;
382
385
  const answer = await options.api(target ? "PATCH" : "POST", `/channels/${destination}/messages${target ? `/${target}` : ""}`, args, file);
383
386
  if (!discordId(answer.id) ||
384
387
  answer.channel_id !== destination ||
package/dist/doctor.js CHANGED
@@ -110,7 +110,7 @@ function reinstallCommand(args) {
110
110
  const freeform = new Set(["--runner", "--repo", "--token-file", "--pool", "--model", "--repair-model", "--plan-model"]);
111
111
  const numeric = new Set(["--max", "--turns", "--tick-every", "--bridge-every", "--reconcile-every", "--max-open-decisions"]);
112
112
  const enums = {
113
- "--provider": ["claude", "codex", "gemini", "openrouter"],
113
+ "--provider": ["claude", "codex", "gemini", "openrouter", "opencode", "cursor", "cline", "qwen", "goose"],
114
114
  "--plan-provider": ["claude", "codex", "gemini", "openrouter"],
115
115
  "--containment": ["observed", "preferred", "required"],
116
116
  };
@@ -0,0 +1,17 @@
1
+ /** The prefix of a bridge folder in the temp folder (agent-fence.ts RUN_FOLDER_PREFIXES denies every other run's). */
2
+ export declare const BRIDGE_PREFIX = "so-net-";
3
+ /** The forwarder, run inside the namespace as `node forward.mjs <folder> <port>... -- <command>`. Zero dependencies. */
4
+ export declare const FORWARDER_SOURCE: string;
5
+ /** Whether a bridge made under `root` keeps every socket path within the limit (its folder and `<port>.sock`). */
6
+ export declare function bridgeFits(root: string): boolean;
7
+ export type EgressBridge = {
8
+ folder: string;
9
+ forwarder: string;
10
+ close: () => Promise<void>;
11
+ };
12
+ /**
13
+ * Serve one socket per port (each relayed to that port on the host's loopback) in a fresh owner-only folder under
14
+ * `root`: the plane's own bridge folder beside the database (agent-fence.ts BRIDGE_DIR), which every build's fence
15
+ * hides from launch, so no other build can change the forwarder or reach the sockets.
16
+ */
17
+ export declare function startEgressBridge(ports: readonly number[], root?: string): Promise<EgressBridge>;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * How a Linux build with a network of its own reaches its proxy and tool broker (agent-fence.ts linuxFenced). The
3
+ * plane serves one unix socket per port in a folder of its own (`<folder>/<port>.sock`, each relayed to that port on
4
+ * the host's loopback); bubblewrap binds the folder read-only into the build's namespace, where the forwarder below
5
+ * listens on the namespace's own 127.0.0.1 at the same ports and relays each connection to its socket. Only those
6
+ * ports cross: nothing else of the host's network is reachable, and the relay never opens a connection of its own.
7
+ *
8
+ * The forwarder is written into the folder at launch (the plane's own file, read-only to the build), so it never runs
9
+ * from an installation path a fence might hide or a build could change.
10
+ */
11
+ import { chmodSync, lstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
+ import { connect, createServer } from "node:net";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ /** The prefix of a bridge folder in the temp folder (agent-fence.ts RUN_FOLDER_PREFIXES denies every other run's). */
16
+ export const BRIDGE_PREFIX = "so-net-";
17
+ /** The forwarder, run inside the namespace as `node forward.mjs <folder> <port>... -- <command>`. Zero dependencies. */
18
+ export const FORWARDER_SOURCE = String.raw `import { connect, createServer } from "node:net";
19
+ import { spawn } from "node:child_process";
20
+ const argv = process.argv.slice(2);
21
+ const split = argv.indexOf("--");
22
+ if (split < 1 || split === argv.length - 1) { process.stderr.write("toolroll forwarder: usage <folder> <port>... -- <command>\n"); process.exit(2); }
23
+ const folder = argv[0];
24
+ const ports = argv.slice(1, split).map(Number);
25
+ const command = argv.slice(split + 1);
26
+ let ready = 0;
27
+ for (const port of ports) {
28
+ const server = createServer(client => {
29
+ const upstream = connect(folder + "/" + port + ".sock");
30
+ client.on("error", () => upstream.destroy());
31
+ upstream.on("error", () => client.destroy());
32
+ client.pipe(upstream);
33
+ upstream.pipe(client);
34
+ });
35
+ server.on("error", error => { process.stderr.write("toolroll forwarder: " + error.message + "\n"); process.exit(70); });
36
+ server.listen(port, "127.0.0.1", () => { if (++ready === ports.length) start(); });
37
+ }
38
+ if (ports.length === 0) start();
39
+ function start() {
40
+ const child = spawn(command[0], command.slice(1), { stdio: "inherit" });
41
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(signal, () => child.kill(signal));
42
+ child.on("error", error => { process.stderr.write("toolroll forwarder: " + error.message + "\n"); process.exit(127); });
43
+ child.on("exit", (code, signal) => {
44
+ if (signal === null) process.exit(code ?? 1);
45
+ // Die the way the agent died: this process's own handlers (which only pass a signal on) step aside first.
46
+ process.removeAllListeners(signal);
47
+ process.kill(process.pid, signal);
48
+ setTimeout(() => process.exit(128), 1000).unref();
49
+ });
50
+ }
51
+ `;
52
+ /** The longest a unix socket's path may be here (macOS 104 bytes, Linux 108, with room to spare). */
53
+ const SOCKET_PATH_MAX = 100;
54
+ /** Whether a bridge made under `root` keeps every socket path within the limit (its folder and `<port>.sock`). */
55
+ export function bridgeFits(root) {
56
+ return join(root, `${BRIDGE_PREFIX}XXXXXX`, "65535.sock").length <= SOCKET_PATH_MAX;
57
+ }
58
+ /**
59
+ * Serve one socket per port (each relayed to that port on the host's loopback) in a fresh owner-only folder under
60
+ * `root`: the plane's own bridge folder beside the database (agent-fence.ts BRIDGE_DIR), which every build's fence
61
+ * hides from launch, so no other build can change the forwarder or reach the sockets.
62
+ */
63
+ export async function startEgressBridge(ports, root = tmpdir()) {
64
+ // The root must be a real folder of this user's alone, never a link a build could have planted before it existed.
65
+ const held = lstatSync(root);
66
+ if (held.isSymbolicLink() || !held.isDirectory() || (process.getuid !== undefined && held.uid !== process.getuid()) || (root !== tmpdir() && (held.mode & 0o077) !== 0)) {
67
+ throw new Error(`the bridge folder ${root} isn't a private folder of this user's`);
68
+ }
69
+ if (!bridgeFits(root))
70
+ throw new Error(`the bridge folder ${root} is too deep for a socket path`);
71
+ const folder = mkdtempSync(join(root, BRIDGE_PREFIX));
72
+ chmodSync(folder, 0o700);
73
+ const forwarder = join(folder, "forward.mjs");
74
+ writeFileSync(forwarder, FORWARDER_SOURCE, { mode: 0o500, flag: "wx" });
75
+ const servers = [];
76
+ const sockets = new Set();
77
+ try {
78
+ for (const port of ports) {
79
+ const server = createServer(client => {
80
+ sockets.add(client);
81
+ const upstream = connect(port, "127.0.0.1");
82
+ sockets.add(upstream);
83
+ const drop = () => { client.destroy(); upstream.destroy(); };
84
+ client.on("error", drop);
85
+ upstream.on("error", drop);
86
+ client.on("close", () => { sockets.delete(client); upstream.destroy(); });
87
+ upstream.on("close", () => { sockets.delete(upstream); client.destroy(); });
88
+ client.pipe(upstream);
89
+ upstream.pipe(client);
90
+ });
91
+ await new Promise((done, fail) => { server.once("error", fail); server.listen(join(folder, `${port}.sock`), () => { server.off("error", fail); done(); }); });
92
+ servers.push(server);
93
+ }
94
+ }
95
+ catch (error) {
96
+ for (const server of servers)
97
+ server.close();
98
+ rmSync(folder, { recursive: true, force: true });
99
+ throw error;
100
+ }
101
+ return {
102
+ folder,
103
+ forwarder,
104
+ close: async () => {
105
+ for (const socket of sockets)
106
+ socket.destroy();
107
+ await Promise.all(servers.map(server => new Promise(done => server.close(() => done()))));
108
+ rmSync(folder, { recursive: true, force: true });
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,119 @@
1
+ import { type Lookup } from "./scout-net.js";
2
+ /** The policy's shape version: a worker enforces the versions it knows and refuses any other. */
3
+ export declare const NETWORK_POLICY_VERSION = 1;
4
+ /** `allowlist`: only the snapshot's hosts, through the run's proxy. `open`: anywhere (an approver's explicit choice). */
5
+ export type NetworkMode = "allowlist" | "open";
6
+ /**
7
+ * What a build may reach on this machine's own loopback. Linux gives every build a loopback of its own, so this only
8
+ * matters on macOS, where one loopback serves everything and the sandbox can't admit some local ports while refusing
9
+ * others: `proxy-only` (the default) admits nothing local but the run's own proxy and tool broker; `any` admits every
10
+ * local service on this Mac — local test servers, and also anything else listening here — an approver's explicit choice.
11
+ */
12
+ export type LoopbackMode = "any" | "proxy-only";
13
+ /** The network a scope was approved with (sealed in its execution profile). */
14
+ export type NetworkProfile = {
15
+ version: 1;
16
+ mode: NetworkMode;
17
+ hosts: string[];
18
+ loopback: LoopbackMode;
19
+ };
20
+ /** A project's own network setting (an approver's): what new scopes of it are written with. */
21
+ export type ProjectNetwork = {
22
+ mode: NetworkMode;
23
+ hosts: string[];
24
+ loopback: LoopbackMode;
25
+ };
26
+ export declare const PROJECT_NETWORK_DEFAULT: ProjectNetwork;
27
+ /** At most this many hosts a project adds (a list longer than this is a different policy: open). */
28
+ export declare const MAX_NETWORK_HOSTS = 200;
29
+ /** At most this many in a sealed snapshot: the project's, plus its provider's and every registry's. */
30
+ export declare const MAX_SNAPSHOT_HOSTS: number;
31
+ /**
32
+ * A host as a policy names it: lowercase ASCII (an international name in its punycode form), no trailing dot, at
33
+ * least two labels, each 1–63 letters, digits or hyphens; or `*.` and such a name, for every name under it. Never an
34
+ * IP address, a port, a path or a bare top-level name. Null when it is none of these.
35
+ */
36
+ export declare function normalizeHost(input: string): string | null;
37
+ /** Whether `host` (as a client named it) is one a policy's list admits: an exact name, or a name under a `*.` entry. */
38
+ export declare function hostAllowed(host: string, hosts: readonly string[]): boolean;
39
+ /**
40
+ * Each provider's own hosts (catalog version 1): its model API and the sign-in refresh its CLI needs. Codex starts
41
+ * its harness outside its command sandbox, so its hosts matter only to what its commands may reach. An ACP agent
42
+ * talks to whatever model service it was set up with; its row names the hosts its own service uses.
43
+ */
44
+ export declare const PROVIDER_HOSTS: Readonly<Record<string, readonly string[]>>;
45
+ /** A package ecosystem a project's files name, and the registry hosts its installs read from. */
46
+ export type Ecosystem = "npm" | "python" | "rust" | "go" | "ruby" | "jvm";
47
+ export declare const REGISTRY_HOSTS: Readonly<Record<Ecosystem, readonly string[]>>;
48
+ /** The ecosystems a list of repository paths names (requirements-*.txt counts as Python). */
49
+ export declare function ecosystemsOf(paths: readonly string[]): Ecosystem[];
50
+ /**
51
+ * The ecosystems the project's own checkout names: its files a few folders deep, read straight from the folder the
52
+ * plane registered (which no build can write: agent-fence.ts taskBoundary), never a build's worktree. No process is
53
+ * started and nothing in the repository runs. An unreadable folder names none: its builds reach the provider and the
54
+ * project's own hosts only.
55
+ */
56
+ export declare function repositoryEcosystems(repo: string): Ecosystem[];
57
+ /** The network a new scope is written with: the project's setting, its provider's hosts and its ecosystems' registries. */
58
+ export declare function resolveNetwork(provider: string, project: ProjectNetwork, ecosystems: readonly Ecosystem[]): NetworkProfile;
59
+ /** The network of a launch nothing sealed one for (a planner, a check): its provider's own hosts and nothing else. */
60
+ export declare function providerOnlyNetwork(provider: string): NetworkProfile;
61
+ /**
62
+ * The network an approval sealed before network policies existed runs under: a deliberate narrowing (provider hosts
63
+ * and the repository's registries, nothing the project added since, local services of this Mac out of reach). Never
64
+ * widened by a setting changed after it was approved.
65
+ */
66
+ export declare function legacyNetwork(provider: string, ecosystems: readonly Ecosystem[]): NetworkProfile;
67
+ /** Strict re-hydration (scope.ts profileFromJson): exactly the keys this code writes, every host normalized, or null. */
68
+ export declare function networkFromValue(value: unknown): NetworkProfile | null;
69
+ /** A project setting as saved (store.ts project_network), or null when it isn't one this code wrote. */
70
+ export declare function projectNetworkFromValue(value: unknown): ProjectNetwork | null;
71
+ /** The network in plain words, for an approval card and a run's record. */
72
+ export declare function networkWords(network: NetworkProfile | null): string;
73
+ /** What the run's proxy saw: connections it made and refused, by host (at most HOST_LIMIT hosts, the rest summed). */
74
+ export type EgressCounts = {
75
+ connections: number;
76
+ refused: number;
77
+ hosts: {
78
+ host: string;
79
+ connections: number;
80
+ refused: number;
81
+ }[];
82
+ otherHosts: number;
83
+ };
84
+ /** What a run's record keeps about its network: the mode it ran under, whether a caller's own proxy carried it, how many
85
+ * hosts its list held, and what the run's proxy saw. */
86
+ export type RunNetworkRecord = {
87
+ mode: "open" | "allowlist";
88
+ phase: "agent";
89
+ via?: true;
90
+ hosts?: number;
91
+ loopback?: LoopbackMode;
92
+ counts?: EgressCounts;
93
+ };
94
+ /** A stored record read back strictly: anything this code didn't write reads as none. */
95
+ export declare function runNetworkFromValue(value: unknown): RunNetworkRecord | null;
96
+ export type EgressProxy = {
97
+ /** The proxy's address with this run's credentials, for HTTPS_PROXY and the like. */
98
+ url: string;
99
+ port: number;
100
+ counts: () => EgressCounts;
101
+ close: () => Promise<void>;
102
+ };
103
+ /** Every proxy setting a program might read, both cases, so none falls back to a direct connection the OS refuses. */
104
+ export declare function proxyEnv(proxy: EgressProxy, loopback: LoopbackMode): Record<string, string>;
105
+ /**
106
+ * Start one run's proxy on loopback. Only a client presenting this run's credentials is served (another run's agent
107
+ * on the same machine can't borrow it). CONNECT goes only to port 443 and plain HTTP only to port 80, only to a host
108
+ * the run's list admits (matched by name before it is resolved), only to public addresses (the proxy resolves the
109
+ * name itself and connects to exactly the addresses it checked). A plain HTTP request's Host must be the URL's host.
110
+ */
111
+ export declare function startEgressProxy(options: {
112
+ hosts: readonly string[];
113
+ lookup?: Lookup;
114
+ decided?: (host: string, allowed: boolean, why: string) => void;
115
+ /** Tests only: which resolved addresses may be reached (default: public internet addresses), and the port an
116
+ * approved request is relayed to (default: the one it asked for). */
117
+ addressAllowed?: (address: string) => boolean;
118
+ upstreamPort?: (port: number) => number;
119
+ }): Promise<EgressProxy>;