toolroll 0.9.63 → 0.9.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
29
29
  import { scanForSecrets } from "./evidence.js";
30
30
  import { namedPath } from "./names.js";
31
31
  import { startToolBroker } from "./tool-broker.js";
32
+ import { isAcpProvider } from "./acp-agents.js";
32
33
  import { SecretStoreError, destroyOwnFile, prepareSecretFolder, registerSecretFolder, pathIn, readOwnFile, readSealed, removeValue, secretBackend, secretFolder, secretStorage, serviceOf, withSecretLock, withSecretLockAsync, writeOwnFile, writeSealed } from "./tool-secret-store.js";
33
34
  import { ACCESS, GRANTED, REFRESH, mintRunToken, mintsRunTokens, oneAtATime, runScope, scopeList, signedIn } from "./oauth-run-token.js";
34
35
  import { HEADER_NAME, SECRET_NAME, TOOL_NAME as NAME, TOOL_SPEC_LIMITS as LIMITS, readToolSpec } from "./contracts/integration-metadata.js";
@@ -710,7 +711,7 @@ export async function claudeToolArgs(launch, options = {}) {
710
711
  omitEnv: [],
711
712
  cleanup,
712
713
  privateDir: dir,
713
- ...(broker === null ? {} : { serverPids: broker.pids }),
714
+ ...(broker === null ? {} : { serverPids: broker.pids, brokerPort: broker.port }),
714
715
  };
715
716
  }
716
717
  /** Where the tool launcher lives beside this module, built. */
@@ -749,7 +750,7 @@ export async function codexToolArgs(launch, options = {}) {
749
750
  if (enabled !== undefined)
750
751
  argv.push("-c", `${key}.enabled_tools=${tomlList(enabled)}`);
751
752
  }
752
- return { argv, env: {}, omitEnv: [], cleanup, privateDir: dir, serverPids: broker.pids };
753
+ return { argv, env: {}, omitEnv: [], cleanup, privateDir: dir, serverPids: broker.pids, brokerPort: broker.port };
753
754
  }
754
755
  /** Gemini: no way yet to hand it exactly these servers, so it gets none. */
755
756
  export const GEMINI_NO_TOOLS_ARGV = ["--allowed-mcp-server-names", "standing-orders-none"];
@@ -846,8 +847,9 @@ export function secretToolRefusal(provider, containment) {
846
847
  export async function prepareRunTools(store, runId, provider, options) {
847
848
  const launch = limitedTo(toolLaunchFor(store, runId, options.home), options.readOnly, options.withheld);
848
849
  // A local server with secrets starts only where the build can't read its environment; the rest are left out,
849
- // saying why, before the broker exists. Gemini starts none (below).
850
- const refusal = provider === "gemini" ? null : secretToolRefusal(provider, options.containment);
850
+ // saying why, before the broker exists. Gemini and ACP agents start none (below).
851
+ const toolless = provider === "gemini" || isAcpProvider(provider);
852
+ const refusal = toolless ? null : secretToolRefusal(provider, options.containment);
851
853
  if (refusal !== null) {
852
854
  launch.tools = launch.tools.filter(one => {
853
855
  if (one.spec.transport === "http" || Object.keys(one.values).length === 0)
@@ -856,14 +858,15 @@ export async function prepareRunTools(store, runId, provider, options) {
856
858
  return false;
857
859
  });
858
860
  }
859
- const revokes = provider === "gemini" ? [] : await signInRun(launch, { readOnly: options.readOnly !== undefined, now: options.now, ...(options.home === undefined ? {} : { home: options.home }), ...(options.fetcher === undefined ? {} : { fetcher: options.fetcher }) });
861
+ const revokes = toolless ? [] : await signInRun(launch, { readOnly: options.readOnly !== undefined, now: options.now, ...(options.home === undefined ? {} : { home: options.home }), ...(options.fetcher === undefined ? {} : { fetcher: options.fetcher }) });
860
862
  const revoke = () => Promise.all(revokes.splice(0).map(one => one())).then(() => undefined);
861
863
  const offered = { ...(options.readOnly === undefined ? {} : { enabledTools: options.readOnly }), ...(options.broker === undefined ? {} : { broker: options.broker }) };
862
864
  let launched;
863
865
  try {
864
866
  launched = provider === "claude" ? await claudeToolArgs(launch, offered)
865
867
  : provider === "codex" || provider === "openrouter" ? await codexToolArgs(launch, offered)
866
- : { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined };
868
+ : provider === "gemini" ? { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined }
869
+ : { argv: [], env: {}, omitEnv: [], cleanup: () => undefined };
867
870
  }
868
871
  catch (error) {
869
872
  // Tokens were made for a run that won't start: each is revoked, and the run with them.
@@ -874,13 +877,13 @@ export async function prepareRunTools(store, runId, provider, options) {
874
877
  }
875
878
  // The run's own sign-ins end with it: the broker closes first, so nothing uses a token as it is revoked.
876
879
  const prepared = revokes.length === 0 ? launched : { ...launched, cleanup: () => { launched.cleanup(); void revoke(); } };
877
- const skipped = provider === "gemini"
878
- ? [...launch.skipped, ...launch.tools.map(one => ({ name: one.spec.name, reason: "Gemini builds can't use project tools yet" }))]
880
+ const skipped = toolless
881
+ ? [...launch.skipped, ...launch.tools.map(one => ({ name: one.spec.name, reason: provider === "gemini" ? "Gemini builds can't use project tools yet" : "ACP agents can't use project tools yet" }))]
879
882
  : launch.skipped;
880
883
  try {
881
884
  store.recordRunTools(runId, JSON.stringify({
882
885
  provider,
883
- tools: provider === "gemini" ? [] : launch.tools.map(one => ({ name: one.spec.name, digest: one.digest })),
886
+ tools: toolless ? [] : launch.tools.map(one => ({ name: one.spec.name, digest: one.digest })),
884
887
  skipped,
885
888
  }), options.now);
886
889
  }
@@ -14,16 +14,17 @@
14
14
  */
15
15
  import type { Store } from "./store.js";
16
16
  import { type ProviderId } from "./provider.js";
17
+ import type { AuthMode } from "./keys.js";
17
18
  /** Every sign-in message's dedupe key starts here: `signin:<kind>:<provider>:<ordinal>:<person>`.
18
19
  * Not a lifecycle (`life:`) key: those are progress chatter, and an undelivered
19
20
  * one stays out of the attention list — this one needs a person. */
20
21
  export declare const SIGN_IN_KEY_PREFIX = "signin:";
21
- export declare const PROVIDER_AUTH_SCHEMA = "\nCREATE TABLE IF NOT EXISTS provider_auth_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key')),\n opened_at TEXT NOT NULL,\n first_run INTEGER,\n task_ref INTEGER,\n runs INTEGER NOT NULL DEFAULT 1,\n -- 1 once a sign-in probe during this pause said the provider is signed out:\n -- only a probe that then sees it signed in again may lift the pause.\n probe_saw_out INTEGER NOT NULL DEFAULT 0,\n lifted_at TEXT,\n lifted_by TEXT,\n lifted_how TEXT CHECK (lifted_how IS NULL OR lifted_how IN ('run', 'probe', 'person')),\n resumed INTEGER,\n -- The last task let through as a trial while paused: a sign-in check can\n -- say \"logged in\" for a session that no longer works, so a real run decides.\n last_trial_at TEXT,\n UNIQUE (provider, ordinal)\n);\nCREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;\n";
22
+ export declare const PROVIDER_AUTH_SCHEMA = "\nCREATE TABLE IF NOT EXISTS provider_auth_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key', 'agent-login')),\n opened_at TEXT NOT NULL,\n first_run INTEGER,\n task_ref INTEGER,\n runs INTEGER NOT NULL DEFAULT 1,\n -- 1 once a sign-in probe during this pause said the provider is signed out:\n -- only a probe that then sees it signed in again may lift the pause.\n probe_saw_out INTEGER NOT NULL DEFAULT 0,\n lifted_at TEXT,\n lifted_by TEXT,\n lifted_how TEXT CHECK (lifted_how IS NULL OR lifted_how IN ('run', 'probe', 'person')),\n resumed INTEGER,\n -- The last task let through as a trial while paused: a sign-in check can\n -- say \"logged in\" for a session that no longer works, so a real run decides.\n last_trial_at TEXT,\n UNIQUE (provider, ordinal)\n);\nCREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;\n";
22
23
  export type AuthPause = {
23
24
  id: number;
24
25
  provider: ProviderId;
25
26
  ordinal: number;
26
- authMode: "subscription" | "api-key";
27
+ authMode: AuthMode;
27
28
  openedAt: string;
28
29
  firstRun: number | null;
29
30
  taskRef: number | null;
@@ -63,7 +64,7 @@ export declare function openAuthPauses(store: Store): AuthPause[];
63
64
  */
64
65
  export declare function pauseForAuth(store: Store, args: {
65
66
  provider: string;
66
- authMode: "subscription" | "api-key" | null;
67
+ authMode: AuthMode | null;
67
68
  runId: number;
68
69
  taskRef: number;
69
70
  now: Date;
@@ -1,4 +1,5 @@
1
1
  import { PROVIDER_IDS } from "./provider.js";
2
+ import { withAcp } from "./acp-agents.js";
2
3
  /** Every sign-in message's dedupe key starts here: `signin:<kind>:<provider>:<ordinal>:<person>`.
3
4
  * Not a lifecycle (`life:`) key: those are progress chatter, and an undelivered
4
5
  * one stays out of the attention list — this one needs a person. */
@@ -8,7 +9,7 @@ CREATE TABLE IF NOT EXISTS provider_auth_pause (
8
9
  id INTEGER PRIMARY KEY AUTOINCREMENT,
9
10
  provider TEXT NOT NULL,
10
11
  ordinal INTEGER NOT NULL,
11
- auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key')),
12
+ auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key', 'agent-login')),
12
13
  opened_at TEXT NOT NULL,
13
14
  first_run INTEGER,
14
15
  task_ref INTEGER,
@@ -27,8 +28,8 @@ CREATE TABLE IF NOT EXISTS provider_auth_pause (
27
28
  );
28
29
  CREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;
29
30
  `;
30
- const NAMES = { claude: "Claude", codex: "Codex", openrouter: "OpenRouter", gemini: "Gemini" };
31
- const SIGN_IN = { claude: "claude /login", codex: "codex login", gemini: "gemini", openrouter: "toolroll keys set openrouter" };
31
+ const NAMES = withAcp({ claude: "Claude", codex: "Codex", openrouter: "OpenRouter", gemini: "Gemini" }, agent => agent.name);
32
+ const SIGN_IN = withAcp({ claude: "claude /login", codex: "codex login", gemini: "gemini", openrouter: "toolroll keys set openrouter" }, agent => agent.signIn);
32
33
  export function providerName(provider) {
33
34
  return PROVIDER_IDS.includes(provider) ? NAMES[provider] : provider;
34
35
  }
@@ -57,7 +58,7 @@ function readPause(row) {
57
58
  id: Number(row["id"]),
58
59
  provider: String(row["provider"]),
59
60
  ordinal: Number(row["ordinal"]),
60
- authMode: row["auth_mode"] === "api-key" ? "api-key" : "subscription",
61
+ authMode: row["auth_mode"] === "api-key" ? "api-key" : row["auth_mode"] === "agent-login" ? "agent-login" : "subscription",
61
62
  openedAt: String(row["opened_at"]),
62
63
  firstRun: row["first_run"] == null ? null : Number(row["first_run"]),
63
64
  taskRef: row["task_ref"] == null ? null : Number(row["task_ref"]),
@@ -1,5 +1,6 @@
1
1
  import { run, type ExecResult } from "./exec.js";
2
2
  import { type ProviderId } from "./provider.js";
3
+ import { type KeyedProviderId } from "./acp-agents.js";
3
4
  import { type AuthMode, type KeyVerdict } from "./keys.js";
4
5
  export type ProviderConnection = {
5
6
  state: "connected" | "signed-out" | "not-installed" | "unverified" | "key-present" | "key-works" | "key-refused" | "missing-key";
@@ -18,5 +19,5 @@ export declare function createConnectionChecker(options?: {
18
19
  home?: string;
19
20
  clock?: () => Date;
20
21
  env?: NodeJS.ProcessEnv;
21
- verify?: (provider: ProviderId, key: string) => Promise<KeyVerdict>;
22
+ verify?: (provider: KeyedProviderId, key: string) => Promise<KeyVerdict>;
22
23
  }): ConnectionChecker;
@@ -2,6 +2,7 @@
2
2
  import { homedir } from "node:os";
3
3
  import { run } from "./exec.js";
4
4
  import { ALL_CREDENTIAL_ENV, inspectionOf } from "./provider.js";
5
+ import { isAcpProvider } from "./acp-agents.js";
5
6
  import { keyStatus, PROVIDER_KEY_ENV, readAuthMode, readProviderKey, verifyProviderKey } from "./keys.js";
6
7
  export function signInFacts(provider, result, now = new Date()) {
7
8
  const base = { mode: "subscription", checkedAt: now.toISOString() };
@@ -46,6 +47,10 @@ export function createConnectionChecker(options = {}) {
46
47
  const cache = new Map();
47
48
  const pending = new Map();
48
49
  return async (provider, fresh = false) => {
50
+ // An ACP agent is never run to ask how it is signed in (nothing executes it outside the gateway): what is known
51
+ // of it is its conformance check, read elsewhere. Here it is simply unverified.
52
+ if (isAcpProvider(provider))
53
+ return { state: "unverified", mode: "agent-login", checkedAt: clock().toISOString() };
49
54
  const mode = readAuthMode(provider, home);
50
55
  const keyFacts = keyStatus(provider, home);
51
56
  const ambient = !!(options.env ?? process.env)[PROVIDER_KEY_ENV[provider]];
@@ -175,6 +175,29 @@ export type ParsedEnvelope = {
175
175
  * failure terminal seen.
176
176
  */
177
177
  structuralTerminal: import("./exhaustion.js").StructuralTerminal | null;
178
+ /** What an ACP agent itself reported it used — kept on the run, never priced. Absent for every other dialect. */
179
+ agentReportedUsage?: string | null;
180
+ /** An ACP conversation's facts the conformance check reads (acp-client.ts AcpEnvelope): nothing else does. */
181
+ acp?: {
182
+ offered: string[];
183
+ exit: {
184
+ how: "eof" | "term" | "kill" | "self" | "error";
185
+ leftover: boolean;
186
+ timedOut: boolean;
187
+ } | null;
188
+ agent: {
189
+ name: string;
190
+ version: string;
191
+ } | null;
192
+ stop: string | null;
193
+ /** The conversation broke the protocol (acpParse's protocolError), or null. */
194
+ protocolError: string | null;
195
+ /** The plane's isolation probes for a check's first step (invoke.ts), when it ran them. */
196
+ isolation?: {
197
+ fileDenied: boolean;
198
+ envClear: boolean;
199
+ };
200
+ };
178
201
  };
179
202
  type Adapter = {
180
203
  binary: string;
@@ -266,13 +289,13 @@ export declare function adapterFor(provider: ProviderId): Adapter;
266
289
  */
267
290
  export type ProviderAudit = {
268
291
  /** How output reaches us — and therefore which signals can exist at all. */
269
- transport: "buffered-json" | "streaming-jsonl";
292
+ transport: "buffered-json" | "streaming-jsonl" | "acp-jsonrpc";
270
293
  /** Whether a later invocation can resume this provider's session. */
271
294
  resume: "native" | "none";
272
295
  /** The event whose absence on a failed run means "never initialized".
273
296
  * CAPABILITY metadata, not a per-run observation (arc 1 finding 16):
274
297
  * whether a given run actually saw it lives in ParsedEnvelope.initObserved. */
275
- initSignal: "thread.started" | "system-init" | "init-event" | "none";
298
+ initSignal: "thread.started" | "system-init" | "init-event" | "acp-session" | "none";
276
299
  /** How session identity is established: "announced" = read back from the
277
300
  * harness's own stream; "minted" = the PLANE chooses the id pre-spawn
278
301
  * (gemini --session-id) and the envelope must echo it (Phase 3 A5). */
package/dist/provider.js CHANGED
@@ -18,13 +18,16 @@
18
18
  * TOML-quoted deterministically, and the API key excluded from every
19
19
  * shell the model itself launches (Codex provider review, Q5).
20
20
  */
21
+ import { INTERPRETER_INJECTION_ENV } from "./acp-attest.js";
21
22
  import { claudeFenceSettings, codexFenceArgv } from "./agent-fence.js";
22
23
  import { claudeSteerHooks } from "./steer-mailbox.js";
23
24
  import {} from "./exec.js";
24
- import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl, CLAUDE_COST_UNKNOWN } from "./exec.js";
25
+ import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl, runAcpSession, CLAUDE_COST_UNKNOWN } from "./exec.js";
25
26
  import { scanForSecrets } from "./evidence.js";
26
27
  import { FINDINGS_MODEL_SCHEMA } from "./contracts/review-findings.js";
27
28
  import { REVIEW_OUTPUT_LIMITS } from "./structured-output.js";
29
+ import { ACP_AGENTS, isAcpProvider, withAcp } from "./acp-agents.js";
30
+ import { acpParse } from "./acp-client.js";
28
31
  import { agentSpecSchema, MODEL_ID, modelIdSchema, PROVIDER_ID_VALUES, providerIdSchema } from "./contracts/provider.js";
29
32
  export const PROVIDER_IDS = PROVIDER_ID_VALUES;
30
33
  export function isProviderId(value) {
@@ -787,12 +790,13 @@ export function safeDiagnostic(text) {
787
790
  * provider gets its OWN keys and nobody else's: S4 (live spike) confirmed
788
791
  * what inheritance makes true by construction — an agent's shell reads its
789
792
  * process env, so a foreign key in that env is a foreign key disclosed. */
790
- const CREDENTIAL_ENV = {
793
+ const CREDENTIAL_ENV = withAcp({
791
794
  claude: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
792
795
  codex: ["OPENAI_API_KEY", "CODEX_API_KEY"],
793
796
  openrouter: [OPENROUTER_ENV_KEY],
794
797
  gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
795
- };
798
+ // An ACP agent signs in its own way; Toolroll holds no key for it, so it sheds every key above.
799
+ }, () => []);
796
800
  const foreignCredentialEnv = (own) => Object.entries(CREDENTIAL_ENV)
797
801
  .filter(([provider]) => provider !== own)
798
802
  .flatMap(([, keys]) => [...keys]);
@@ -800,7 +804,40 @@ const foreignCredentialEnv = (own) => Object.entries(CREDENTIAL_ENV)
800
804
  * since a feature check needs no key at all (Codex gemini verify,
801
805
  * finding 2). Agent spawns keep their own; probes keep none. */
802
806
  export const ALL_CREDENTIAL_ENV = Object.values(CREDENTIAL_ENV).flat();
803
- const ADAPTERS = {
807
+ /**
808
+ * An ACP agent's retained record (acp-client.ts) read into the gateway's
809
+ * envelope. Its own usage report is never priced: tokens and cost stay null.
810
+ */
811
+ function acpEnvelope(stdout) {
812
+ const read = acpParse(stdout);
813
+ return {
814
+ sessionId: read.sessionId,
815
+ protocolError: read.protocolError,
816
+ finalMessage: read.finalMessage,
817
+ tokensIn: null,
818
+ tokensOut: null,
819
+ costUsd: null,
820
+ usageRaw: null,
821
+ initObserved: read.initObserved,
822
+ promptConsumed: read.promptConsumed,
823
+ diagnostic: read.diagnostic === null ? null : safeDiagnostic(read.diagnostic),
824
+ structuralTerminal: read.terminal === null ? null : { failed: true, text: read.terminal.text === null ? null : safeDiagnostic(read.terminal.text), code: read.terminal.code.slice(0, 128) },
825
+ agentReportedUsage: read.reportedUsage,
826
+ acp: { offered: read.offered, exit: read.exit, agent: read.agent, stop: read.stop, protocolError: read.protocolError },
827
+ };
828
+ }
829
+ function acpAdapter(provider) {
830
+ return {
831
+ binary: ACP_AGENTS[provider].binary,
832
+ argv: () => [...ACP_AGENTS[provider].argv],
833
+ parse: acpEnvelope,
834
+ defaultRunner: runAcpSession,
835
+ // Every key Toolroll knows, and whatever would make the pinned interpreter load code beside the checked script.
836
+ extraOmitEnv: [...foreignCredentialEnv(provider), ...INTERPRETER_INJECTION_ENV],
837
+ clampTimeout: (_phase, requested) => requested,
838
+ };
839
+ }
840
+ const ADAPTERS = withAcp({
804
841
  claude: {
805
842
  binary: "claude",
806
843
  argv: claudeArgv,
@@ -852,7 +889,7 @@ const ADAPTERS = {
852
889
  extraOmitEnv: foreignCredentialEnv("gemini"),
853
890
  clampTimeout: (phase, requested) => Math.min(requested, GEMINI_TIMEOUT_CAP_MS[phase]),
854
891
  },
855
- };
892
+ }, (_agent, provider) => acpAdapter(provider));
856
893
  /**
857
894
  * The spawning surface. Imported by invoke.ts and NOWHERE else — the
858
895
  * architecture test reads imports, not string literals, because provider
@@ -861,7 +898,7 @@ const ADAPTERS = {
861
898
  export function adapterFor(provider) {
862
899
  return ADAPTERS[provider];
863
900
  }
864
- const AUDITS = {
901
+ const AUDITS = withAcp({
865
902
  claude: {
866
903
  transport: "streaming-jsonl",
867
904
  resume: "native",
@@ -923,8 +960,17 @@ const AUDITS = {
923
960
  "trusted-folder store (S2, live at 0.57.0: headless REFUSES untrusted directories; the plane grants --skip-trust per-invocation to its own leased worktrees)",
924
961
  ],
925
962
  },
926
- };
927
- export const MONEY_CAPABILITIES = {
963
+ }, agent => ({
964
+ // One ACP conversation per build: the prompt's answer is the only success (acp-client.ts), so exit 0 alone is never believed.
965
+ transport: "acp-jsonrpc",
966
+ resume: "none",
967
+ initSignal: "acp-session",
968
+ sessionIdentity: "announced",
969
+ terminalContract: "required",
970
+ isolation: { flag: agent.argv.filter(one => one.startsWith("--") && one !== "--acp").join(" ") || null, resumeSafe: null, enforced: agent.argv.some(one => one.startsWith("--") && one !== "--acp") },
971
+ configSurface: [...agent.configSurface],
972
+ }));
973
+ export const MONEY_CAPABILITIES = withAcp({
928
974
  claude: {
929
975
  incrementalUsage: true,
930
976
  nativeDollarCapFlag: "--max-budget-usd",
@@ -947,7 +993,12 @@ export const MONEY_CAPABILITIES = {
947
993
  // numbers cover that invocation only (conformance fixture j).
948
994
  usageSemantics: "per-invocation",
949
995
  },
950
- };
996
+ }, () => ({
997
+ // ACP carries no dollars Toolroll trusts: an agent's own usage report is kept apart, unpriced.
998
+ incrementalUsage: false,
999
+ nativeDollarCapFlag: null,
1000
+ usageSemantics: "per-invocation",
1001
+ }));
951
1002
  /**
952
1003
  * The fail-closed budget-flag probe (finding 24's amendment): resolve
953
1004
  * the EXACT executable that will spawn, read its version, and prove the
@@ -997,6 +1048,9 @@ export function validateSpec(spec) {
997
1048
  if (spec.provider === "gemini" && spec.model === null) {
998
1049
  return { ok: false, problem: "model: gemini needs an explicit model — the harness default drifts with its releases" };
999
1050
  }
1051
+ if (isAcpProvider(spec.provider) && spec.model === null) {
1052
+ return { ok: false, problem: `model: ${ACP_AGENTS[spec.provider].name} needs an explicit model — one it offered when it was checked` };
1053
+ }
1000
1054
  return { ok: true };
1001
1055
  }
1002
1056
  /**
@@ -1,11 +1,11 @@
1
1
  /** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
2
2
  * found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
3
3
  * the password; turning on restates the exact repository and branch that were checked. */
4
- import { html, postForm } from "./html.js";
4
+ import { html, postForm, reauth } from "./html.js";
5
5
  const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
6
6
  export function pullRequestSettingsHtml(view) {
7
7
  const post = (act, body, hidden = {}) => postForm("/settings/pull-requests", body, { attrs: { class: "card pr-settings" }, hidden: { repo: view.repo, act, ...hidden } });
8
- const password = html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
8
+ const password = html `${reauth(html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
9
9
  const notes = [
10
10
  view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
11
11
  view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
package/dist/push.js CHANGED
@@ -259,7 +259,7 @@ export async function pushPass(store, options) {
259
259
  // Renew and re-fence immediately before the network (finding 12).
260
260
  if (!store.renewPushClaim(pair.id, owner, pair.claimGeneration, PUSH_CLAIM_TTL_MS, clock()))
261
261
  continue;
262
- const fenced = store.pushSendFence(pair.id, owner, pair.claimGeneration);
262
+ const fenced = store.pushSendFence(pair.id, owner, pair.claimGeneration, clock());
263
263
  if (fenced === null)
264
264
  continue; // resolved, retired, rotated, or lost — nothing to send
265
265
  const subscription = fenced.subscriptionRow;
@@ -0,0 +1,24 @@
1
+ import { type AttachSeams } from "./console-instance.js";
2
+ export type QuickStartSeams = {
3
+ databaseFile: string;
4
+ cwd: string;
5
+ env: Record<string, string | undefined>;
6
+ build: string;
7
+ /** The repository's top folder for a directory, or null outside one. */
8
+ findRepo: (cwd: string) => Promise<string | null>;
9
+ /** Whether this installation already admitted the repository. */
10
+ enrolled: (repo: string) => Promise<boolean>;
11
+ /** The ordinary admission (`repos add`): its registry lock and ceilings, from this CLI's own database. */
12
+ admit: (repo: string) => Promise<number>;
13
+ /** `up`, for this repository. */
14
+ startUp: () => Promise<number>;
15
+ openBrowser: (url: string) => void | Promise<void>;
16
+ terminal: boolean;
17
+ attach?: AttachSeams;
18
+ /** Tests: the agent check (default: the environment and the process's ancestors). */
19
+ underAgent?: boolean;
20
+ };
21
+ /** Whether argv is a bare `toolroll` (nothing, or only --json). */
22
+ export declare const isBareInvocation: (argv: readonly string[]) => boolean;
23
+ /** null: not this command (outside a repository) — the caller scans as before. */
24
+ export declare function runQuickStart(argv: readonly string[], write: (line: string) => void, seams: QuickStartSeams): Promise<number | null>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * BARE `toolroll` IN A REPOSITORY (docs/plans/agents-cockpit.md B1): the two-action start.
3
+ *
4
+ * Inside a git repository, `toolroll` with nothing else (or only --json) either starts the console for it (`up`), or —
5
+ * when this installation's console is already running and PROVES it is (console-instance.ts: a fresh challenge on the
6
+ * discovery socket and on its HTTP listener, answered with the secret in the database this CLI opened itself) —
7
+ * adds the repository through the ordinary project admission and opens that proved console. Under an agent, or with
8
+ * --json, it starts nothing and changes nothing: it says what it would do. Outside a repository, it scans as before
9
+ * (the scan is also `toolroll scan`).
10
+ */
11
+ import { lstatSync, realpathSync } from "node:fs";
12
+ import { proveLiveConsole } from "./console-instance.js";
13
+ import { underAgent } from "./prompt.js";
14
+ import { envelopeJson } from "./envelope.js";
15
+ import { createRequire } from "node:module";
16
+ import { openStoreReadOnly, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
17
+ const EXIT = { ok: 0, refused: 3 };
18
+ /** Whether argv is a bare `toolroll` (nothing, or only --json). */
19
+ export const isBareInvocation = (argv) => argv.every(one => one === "--json");
20
+ /** The database's schema version by the store's own strict reader, read without changing anything. */
21
+ function schemaOf(file) {
22
+ try {
23
+ // Loaded only here, as the store loads it: the CLI's other commands never pay for (or warn about) it.
24
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
25
+ const db = new DatabaseSync(file, { readOnly: true });
26
+ try {
27
+ const read = readSchemaVersion(db);
28
+ return read.ok ? read : { ok: false };
29
+ }
30
+ finally {
31
+ db.close();
32
+ }
33
+ }
34
+ catch {
35
+ return { ok: false };
36
+ }
37
+ }
38
+ /** Whether the database file is there: "absent" only when the system says so (ENOENT), never for a path it won't show. */
39
+ function databasePresence(file) {
40
+ try {
41
+ lstatSync(file);
42
+ return "present";
43
+ }
44
+ catch (error) {
45
+ return error.code === "ENOENT" ? "absent" : "unknown";
46
+ }
47
+ }
48
+ async function prove(seams) {
49
+ // Read only, never created or migrated, never cleaned up: proving changes nothing. "None" only when nothing of this
50
+ // build can be running on it (no database yet, or one older than this build, which `up` brings up to date); a
51
+ // database that can't be read, is mid-upgrade or is newer than this build can't be checked, so it is refused.
52
+ const none = { ok: false, state: "none", message: "no console is running" };
53
+ const unverifiable = (why) => ({ ok: false, state: "refused", message: `${why}, so a running console can't be checked — run \`toolroll up\` to see what's wrong` });
54
+ const presence = databasePresence(seams.databaseFile);
55
+ if (presence === "absent")
56
+ return none;
57
+ if (presence === "unknown")
58
+ return unverifiable("Toolroll's database can't be reached");
59
+ const schema = schemaOf(seams.databaseFile);
60
+ if (!schema.ok)
61
+ return unverifiable("Toolroll's database can't be read");
62
+ if (schema.version === null || (schema.version > 0 && schema.version < SCHEMA_VERSION))
63
+ return none;
64
+ if (schema.version !== SCHEMA_VERSION)
65
+ return unverifiable("Toolroll's database is being upgraded");
66
+ const store = openStoreReadOnly(seams.databaseFile);
67
+ if (store === null)
68
+ return unverifiable("Toolroll's database can't be read");
69
+ try {
70
+ return await proveLiveConsole(seams.databaseFile, store, seams.build, seams.attach ?? {});
71
+ }
72
+ catch {
73
+ return unverifiable("the running console couldn't be checked");
74
+ }
75
+ finally {
76
+ store.close();
77
+ }
78
+ }
79
+ /** null: not this command (outside a repository) — the caller scans as before. */
80
+ export async function runQuickStart(argv, write, seams) {
81
+ const found = await seams.findRepo(seams.cwd);
82
+ if (found === null)
83
+ return null;
84
+ let repo = found;
85
+ try {
86
+ repo = realpathSync(found);
87
+ }
88
+ catch { /* as found */ }
89
+ const json = argv.includes("--json");
90
+ const dry = json || (seams.underAgent ?? underAgent(seams.env));
91
+ const proof = await prove(seams);
92
+ const enrolled = await seams.enrolled(repo);
93
+ const would = proof.ok ? (enrolled ? "open" : "add-and-open") : proof.state === "none" ? "start" : "refuse";
94
+ if (dry) {
95
+ // Nothing starts, nothing is added, nothing opens: what a person would get, in words or one envelope.
96
+ const words = would === "start" ? `Would start Toolroll for ${repo} (toolroll up).`
97
+ : would === "refuse" ? `Would refuse: ${proof.message}.`
98
+ : `Would ${would === "add-and-open" ? `add ${repo} and ` : ""}open the running console at ${proof.url}.`;
99
+ if (json)
100
+ write(envelopeJson({ ok: would !== "refuse", command: "toolroll", repo, would, ...(proof.ok ? { url: proof.url } : { console: proof.state, message: proof.message }), dryRun: true }));
101
+ else
102
+ write(words);
103
+ return would === "refuse" ? EXIT.refused : EXIT.ok;
104
+ }
105
+ if (!proof.ok && proof.state === "refused") {
106
+ write(`Toolroll can't attach: ${proof.message}.`);
107
+ return EXIT.refused;
108
+ }
109
+ if (!proof.ok)
110
+ return seams.startUp();
111
+ if (!enrolled) {
112
+ const admitted = await seams.admit(repo);
113
+ if (admitted !== 0)
114
+ return admitted;
115
+ }
116
+ // Whatever happened since the first proof (reading the registry, adding the project), the console is proved again,
117
+ // on both channels, right before its address is shown or opened — never a listener that took the port meanwhile.
118
+ const again = await prove(seams);
119
+ if (!again.ok || again.url !== proof.url || again.pid !== proof.pid) {
120
+ write(`Toolroll can't attach: ${again.ok ? "the console changed while the project was being added" : again.message} — run \`toolroll up\`.`);
121
+ return EXIT.refused;
122
+ }
123
+ write(`Toolroll is on ${proof.url}`);
124
+ if (seams.terminal)
125
+ await seams.openBrowser(proof.url);
126
+ return EXIT.ok;
127
+ }
package/dist/release.d.ts CHANGED
@@ -226,10 +226,13 @@ export type ReleaseJournal = {
226
226
  by: string;
227
227
  at: string;
228
228
  } | null;
229
+ /** The passing result: its run, receipt and checkout, and the commit it built (`head`), which a prepared candidate
230
+ * records as a commit of its own with exactly the candidate's tree (absent in a journal from before that was known). */
229
231
  check: {
230
232
  run: number;
231
233
  receipt: string;
232
234
  worktree: string;
235
+ head?: string;
233
236
  } | null;
234
237
  completion: {
235
238
  actor: string;
package/dist/release.js CHANGED
@@ -213,9 +213,11 @@ export async function runRelease(options, adapters) {
213
213
  if (gate.result.check !== "passed")
214
214
  throw new Stop("check-failed", `The Full release check of ${j.candidate.sha.slice(0, 7)} did not pass (run ${gate.result.run}: ${gate.result.check}). It stays as it is: fix the branch and release the new commit with --new.`);
215
215
  };
216
+ /** The commit the passing build recorded: what the deployed runtime is stamped with. */
217
+ const builtHead = () => j.check?.head ?? j.candidate.sha;
216
218
  const checked = (gate) => {
217
219
  approved(gate);
218
- if (gate.result?.head !== j.candidate.sha || gate.result?.receipt !== j.check?.receipt)
220
+ if (gate.result?.head !== builtHead() || gate.result?.receipt !== j.check?.receipt)
219
221
  throw new Stop("drift", `Task ${j.task.id}'s result changed since its check passed.`);
220
222
  fullCheck(gate);
221
223
  };
@@ -268,12 +270,16 @@ export async function runRelease(options, adapters) {
268
270
  throw new Stop("check-failed", `The release build of ${j.task.id} failed: ${gate.failed}. Fix it and release the new commit with --new.`);
269
271
  if (gate.result === null || gate.result.check === "running" || gate.result.check === "pending")
270
272
  return null;
271
- if (gate.result.head !== j.candidate.sha)
272
- throw new Stop("drift", `The release result is for ${gate.result.head?.slice(0, 7) ?? "no commit"}, not ${j.candidate.sha.slice(0, 7)}.`);
273
+ // The build records a prepared candidate as a commit of its own (its message names the task): it must hold
274
+ // exactly the candidate's tree, or it isn't the release.
275
+ const head = gate.result.head;
276
+ if (head === null || (head !== j.candidate.sha && await adapters.repo.tree(head) !== await adapters.repo.tree(j.candidate.sha))) {
277
+ throw new Stop("drift", `The release result is for ${head?.slice(0, 7) ?? "no commit"}, which doesn't hold exactly ${j.candidate.sha.slice(0, 7)}'s files.`);
278
+ }
273
279
  fullCheck(gate);
274
280
  if (gate.result.worktree === null)
275
281
  throw new Stop("worktree", `Run ${gate.result.run} names no checkout to deploy from.`);
276
- return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree };
282
+ return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
277
283
  });
278
284
  j.check = result;
279
285
  say(` run ${result.run} passed on ${j.candidate.sha.slice(0, 7)}`);
@@ -310,7 +316,7 @@ export async function runRelease(options, adapters) {
310
316
  await recoverDeploy();
311
317
  checked(await adapters.toolroll.gate(j.task.id));
312
318
  // A deployment that finished but whose answer was lost: the services already run a runtime of this commit.
313
- let runtime = await served(adapters, j.candidate.sha);
319
+ let runtime = await served(adapters, builtHead());
314
320
  if (runtime === null) {
315
321
  await pinned();
316
322
  j.deployAttempt = { stage: join(options.stateDir, "staged-upgrades", `browser-${j.candidate.sha.slice(0, 7)}-${randomUUID()}`) };
@@ -326,7 +332,7 @@ export async function runRelease(options, adapters) {
326
332
  save();
327
333
  }
328
334
  const want = runtime;
329
- await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters, j.candidate.sha)) === want ? true : null);
335
+ await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters, builtHead())) === want ? true : null);
330
336
  j.deploy = { runtime: want, at: j.deploy?.runtime === want ? j.deploy.at : adapters.now().toISOString() };
331
337
  j.deployAttempt = null;
332
338
  delete j.intents.deploy;
@@ -336,7 +342,7 @@ export async function runRelease(options, adapters) {
336
342
  }
337
343
  case "link": {
338
344
  const runtime = j.deploy.runtime;
339
- if (await served(adapters, j.candidate.sha) !== runtime)
345
+ if (await served(adapters, builtHead()) !== runtime)
340
346
  throw new Stop("drift", `The console and worker no longer run ${runtime}; the CLI names were not moved.`);
341
347
  if (j.links === undefined || j.links === null) {
342
348
  j.links = await adapters.toolroll.cliLinks();
@@ -348,7 +354,7 @@ export async function runRelease(options, adapters) {
348
354
  catch (error) {
349
355
  throw new Stop("link", `The CLI names were not moved to ${runtime} (they point where they did): ${error.message}. The service already runs the new build; rerun to try again.`);
350
356
  }
351
- const everywhere = await deployed(adapters, j.candidate.sha);
357
+ const everywhere = await deployed(adapters, builtHead());
352
358
  if (everywhere !== runtime)
353
359
  throw new Stop("mixed-runtime", `After relinking, the console, worker and both CLI names don't all run ${runtime} at ${j.candidate.sha.slice(0, 7)}.`);
354
360
  say(` toolroll and standing-orders run ${join(runtime, "bin.js")}`);
@@ -1,4 +1,4 @@
1
- import { html, postForm } from "./html.js";
1
+ import { html, postForm, reauth } from "./html.js";
2
2
  import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
3
3
  const n = (value) => value.toLocaleString("en-US");
4
4
  export const REQUEST_LIMITS_CSS = `.credentials .limits{display:grid;gap:14px}.credentials .limits form{display:grid;gap:10px}.credentials .limits label{display:grid;gap:6px}` +
@@ -14,7 +14,7 @@ export function requestLimitsHtml(db, tokens, now) {
14
14
  const overrides = limitOverrides(db);
15
15
  const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
16
16
  const field = (name, label, value, placeholder, max) => html `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
17
- const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`;
17
+ const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}`;
18
18
  const buttons = html `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
19
19
  const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
20
20
  const installation = postForm("/settings/request-limits", html `\