toolroll 0.9.72 → 0.9.73

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 (63) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/agent-fence.d.ts +5 -0
  3. package/dist/agent-fence.js +42 -7
  4. package/dist/browser/workspace.js +28 -28
  5. package/dist/chat-actions.js +13 -8
  6. package/dist/chat-decide.d.ts +47 -1
  7. package/dist/chat-decide.js +170 -4
  8. package/dist/check-report.js +2 -1
  9. package/dist/coding-provider.js +4 -2
  10. package/dist/commit-extract.d.ts +17 -0
  11. package/dist/commit-extract.js +82 -4
  12. package/dist/contracts/console-api.d.ts +3 -3
  13. package/dist/contracts/console-api.js +4 -4
  14. package/dist/contracts/release-terms.d.ts +7 -1
  15. package/dist/contracts/release-terms.js +6 -2
  16. package/dist/exec.d.ts +1 -1
  17. package/dist/exec.js +7 -3
  18. package/dist/flow-gallery-ui.js +4 -2
  19. package/dist/flow-gallery.d.ts +3 -0
  20. package/dist/flow-gallery.js +6 -2
  21. package/dist/flow-triggers.d.ts +8 -0
  22. package/dist/flow-triggers.js +15 -0
  23. package/dist/flows-cli.js +13 -3
  24. package/dist/guides.js +10 -1
  25. package/dist/kits-ui.js +4 -2
  26. package/dist/kits.d.ts +6 -4
  27. package/dist/kits.js +9 -11
  28. package/dist/mcp-connect.d.ts +10 -0
  29. package/dist/mcp-connect.js +2 -0
  30. package/dist/project-tools.js +3 -1
  31. package/dist/protected-storage.d.ts +67 -0
  32. package/dist/protected-storage.js +398 -0
  33. package/dist/release-adapters.d.ts +8 -1
  34. package/dist/release-adapters.js +80 -14
  35. package/dist/release-authority.d.ts +6 -0
  36. package/dist/release-authority.js +9 -0
  37. package/dist/release-card.d.ts +76 -0
  38. package/dist/release-card.js +252 -0
  39. package/dist/release-carry.d.ts +3 -0
  40. package/dist/release-carry.js +23 -4
  41. package/dist/release-cli.js +36 -3
  42. package/dist/release-git.d.ts +43 -0
  43. package/dist/release-git.js +106 -0
  44. package/dist/release.d.ts +8 -2
  45. package/dist/release.js +79 -6
  46. package/dist/scope.d.ts +7 -0
  47. package/dist/scope.js +22 -5
  48. package/dist/server/flows.js +40 -10
  49. package/dist/server/render-settings.js +1 -1
  50. package/dist/server/settings.js +26 -5
  51. package/dist/setup-guide.d.ts +2 -2
  52. package/dist/store.d.ts +3 -0
  53. package/dist/store.js +6 -0
  54. package/dist/subagent-memory.d.ts +1 -0
  55. package/dist/subagent-memory.js +1 -1
  56. package/dist/subagent-tools.d.ts +22 -6
  57. package/dist/subagent-tools.js +44 -8
  58. package/dist/subagent-work.d.ts +2 -0
  59. package/dist/subagent-work.js +5 -2
  60. package/dist/subagents-ui.js +6 -4
  61. package/dist/telegram.d.ts +6 -1
  62. package/dist/telegram.js +58 -6
  63. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.73 — 2026-10-25
4
+
5
+ - **Carry release approvals when checks aren't sandboxed.** A project whose
6
+ checks run outside the sandbox (like Toolroll's own release project) can
7
+ let later test-only commits carry a release approval:
8
+ `toolroll release unsandboxed-carry on --repo <path> --as <you> --token
9
+ <password>`. It is off by default; only a person turns it on, and it counts
10
+ only for releases approved while it is on. The approval sheet says plainly
11
+ that those later commits will run outside the sandbox without being shown
12
+ to you first. Every other carry rule stays: tests and docs only, the
13
+ 400-line, 20-file and 800-line-churn limits, an identical production build,
14
+ and main's open nightly failures. Turning it off, or checks that reach
15
+ further than at the approval, stops carrying.
16
+ - **`toolroll release` stops at the gate when the pull request's checks
17
+ fail.** Before it files the release task or asks for your approval, it
18
+ reads the required checks on the exact commit. One that failed or was
19
+ cancelled stops the release with the failing job names, so you aren't
20
+ asked to approve a commit that can't merge. Checks not started or still
21
+ running are named and waited for; if they don't finish within the gate's
22
+ limit (60 minutes, `--limit gate=<min>`), it stops there too. The ci step
23
+ still checks every one again before merging.
24
+ - **Chat apps, browsers and password managers are off limits to every
25
+ launch.** Builds, checks, flows, tool servers and coding sessions can't
26
+ read or write where Slack, Telegram, Discord, Teams, WhatsApp, 1Password,
27
+ Bitwarden, or the Chrome, Chromium, Brave, Edge, Arc, Firefox, Zen,
28
+ Vivaldi, Opera, Orion and Safari profiles keep their data, on macOS and
29
+ Linux, under both sandbox versions and in Codex's sandbox. This covers the
30
+ folders by name and by real path, links inside them, and a moved profile
31
+ or config folder named in the environment. No project setting reopens
32
+ them, and those apps' variables (SLACK_, TELEGRAM_, DISCORD_, TEAMS_, OP_,
33
+ BW_, BITWARDENCLI_) are removed from every launch; a tool server keeps the
34
+ secrets it names.
35
+ - **`toolroll release` runs Git safely on the release checkout.** The release
36
+ used to run plain Git there, so a setting planted in the checkout's Git
37
+ config (an include, a hook folder, an ssh command, a proxy, a URL rewrite
38
+ and the like) could run a command or send the gh token elsewhere. Now it
39
+ uses the repository and GitHub URL from Toolroll's own records, runs Git,
40
+ gh and ssh by absolute path with a fixed environment, pins HTTPS settings
41
+ on the command line, and allows only the plain settings Git writes itself.
42
+ Any other setting stops the release and names it. The checkout must be a
43
+ project Toolroll records, with a recorded remote.
44
+ - **Widening actions take your password.** Setting the permission default to
45
+ Full access, giving a subagent a tool, loosening a tool rule, accepting a
46
+ subagent's suggested looser rule, and adding a trigger that brings in work
47
+ from anyone (GitHub from anyone, mail from any sender, from the Triggers
48
+ panel, a gallery template or a starter kit) now ask for your password (a
49
+ signed-in session does under Passwordless). Narrowing needs none. A chat
50
+ card, import or template can no longer do any of these; it points to the
51
+ page that can.
52
+ - **Approving a release from chat shows the whole release first.** A tap
53
+ approves a deploy only through the release card: every signed term and
54
+ release term, one line each (it says so when later test-only commits
55
+ will run outside the sandbox), and the commits since the last release
56
+ Toolroll recorded as merged. Continue sends the terms (as a file where the
57
+ chat app allows), and Approve is bound to exactly what was shown;
58
+ anything changed in between refuses with the new card. It works only in
59
+ your own private chat (Telegram or a Slack direct message); a group,
60
+ someone else's chat, Discord and Teams are refused. Each such approval is
61
+ announced to you, naming the chat app.
62
+ - **Network, writes, check access and sandbox settings stay on the typed
63
+ password under Passwordless,** and a test now fails if one moves to the
64
+ ordinary signed-in step-up.
65
+ - **Launches don't wait on macOS's "access data from other apps" prompt.**
66
+ Toolroll looks inside other apps' containers only after a 3-second look in
67
+ a separate process shows macOS lets it, so a service run by launchd no
68
+ longer hangs every fenced launch. While macOS is still asking, the
69
+ containers stay denied by name and real path, and the links found by the
70
+ last full look stay denied; a look that fails for any other reason refuses
71
+ the launch.
72
+ - **The release check ends itself before its time limit,** stopping the
73
+ parts still running and keeping every log and its report, and a failed
74
+ part's log reaches the run's output as soon as it fails.
75
+ - **`toolroll release` rides out short GitHub network timeouts** while a
76
+ step waits, asking again until that step's own limit.
77
+ - No database change: the schema stays at v135.
78
+
3
79
  ## 0.9.72 — 2026-10-25
4
80
 
5
81
  - **One approval per release.** A person approves a release once. Later
@@ -29,6 +29,8 @@ export declare function agentFence(options: {
29
29
  boundary?: TaskBoundary | null;
30
30
  binding?: WorktreeBinding | null;
31
31
  runFolders?: RunFolders;
32
+ /** Protected storage as this launch resolved it (protected-storage.ts); resolved here when not given. */
33
+ protectedStorage?: readonly string[];
32
34
  }): string[];
33
35
  /** The task's boundary: `shell` joins the OS fence; `fileTools` only Claude's file-tool deny rules; `readOnly` and
34
36
  * `lookup` are other tasks' git metadata and checkout roots (see FenceGrants). */
@@ -103,6 +105,9 @@ export type FenceGrants = {
103
105
  signIns?: SignInGrants;
104
106
  /** Never written, wherever the launch writes (homeWriteDenies): what runs for the operator next or signs them in. */
105
107
  writeDenies?: string[];
108
+ /** Protected storage (protected-storage.ts): denied to read and write by every backend, and no grant equal to or
109
+ * inside one survives (a sandbox that lets the more specific path win can't reopen it). */
110
+ protectedStorage?: string[];
106
111
  };
107
112
  export { isWritesMode, WRITES_DEFAULT, type WritesMode } from "./writes-mode.js";
108
113
  /**
@@ -34,6 +34,7 @@ import { readMonitoring } from "./monitoring-settings.js";
34
34
  import { existingFolders } from "./names.js";
35
35
  import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
36
36
  import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
37
+ import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
37
38
  import { getDefaultWritePaths } from "@anthropic-ai/sandbox-runtime";
38
39
  /** Names that are secrets wherever the database lives (used when its folder is shared with other things). */
39
40
  const SENSITIVE_NAME = /(^up-login\.txt$|token|secret|login|password|credential|vapid|keys?\b|\.pem$|\.key$|^backups$|^evidence$|^remote$|^sign-in\.json$|^monitoring\.json$)/i;
@@ -188,11 +189,18 @@ export function agentFence(options) {
188
189
  for (const path of options.boundary?.protectedShell ?? [])
189
190
  fence.add(real(path));
190
191
  const protectedPaths = new Set([...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]));
192
+ // Protected storage (chat apps, browser profiles, password managers): every launch, every policy, never exemptible,
193
+ // kept by the name it was resolved under as well as where it leads.
194
+ const storage = options.protectedStorage ?? protectedStorageDenies(home);
195
+ for (const path of storage) {
196
+ protectedPaths.add(path);
197
+ protectedPaths.add(real(path));
198
+ }
191
199
  for (const path of options.boundary?.shell ?? [])
192
200
  fence.add(path);
193
201
  const binding = options.binding;
194
202
  const needed = binding === undefined || binding === null ? [] : [binding.gitdir, real(join(binding.main, ".git"))];
195
- return [...new Set([...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]))].filter(path => {
203
+ return [...new Set([...[...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
196
204
  if (protectedPaths.has(path) || !needed.some(target => target === path || inside(target, path)))
197
205
  return true;
198
206
  console.warn(`Dropped build fence entry covering this checkout's own Git data: ${path}`);
@@ -237,10 +245,12 @@ export function taskBoundary(options) {
237
245
  const present = (names) => names.map(name => join(home, name)).filter(path => existsSync(path)).map(real);
238
246
  // Policy v2 (docs/plans/sandbox-parity.md §1.2): the home folder, the repositories beside this one and the project's
239
247
  // primary checkout are read as Anthropic's sandbox runtime reads them; only writes to the primary checkout stay denied.
248
+ // Protected storage by name, under both policies (protected-storage.ts): never part of the v1-only home configuration.
249
+ const storage = protectedStorageNames(home);
240
250
  if ((options.policy ?? 1) === 2)
241
- return v2Boundary(worktree, main, gitdir);
251
+ return v2Boundary(worktree, main, gitdir, storage);
242
252
  const shell = new Set(present(HOME_CONFIGURATION.filter(name => !own.has(name))));
243
- const protectedShell = [...shell];
253
+ const protectedShell = [...shell, ...storage];
244
254
  const repository = (path) => existsSync(join(path, ".git"));
245
255
  for (const parent of new Set([dirname(worktree), ...(main === null ? [] : [dirname(main)])])) {
246
256
  // Home contains toolchain clones such as .nvm and .oh-my-zsh. The
@@ -290,7 +300,7 @@ export function taskBoundary(options) {
290
300
  /** A v2 boundary: nothing of the home folder, the repositories beside this one or the primary checkout is denied to
291
301
  * read; the primary checkout (all but its `.git`, which this worktree's Git needs) is never written; other tasks' Git
292
302
  * metadata is read, never written, as under v1. */
293
- function v2Boundary(worktree, main, gitdir) {
303
+ function v2Boundary(worktree, main, gitdir, storage) {
294
304
  const noWrite = new Set();
295
305
  if (main !== null && main !== worktree) {
296
306
  let entries = [];
@@ -309,7 +319,7 @@ function v2Boundary(worktree, main, gitdir) {
309
319
  const { readOnly, lookup } = siblingGitData(main, gitdir);
310
320
  for (const path of noWrite)
311
321
  readOnly.add(path);
312
- return { protectedShell: [], shell: [], fileTools: [], readOnly: [...readOnly].sort(), lookup: [...lookup].sort(), homeNames: [] };
322
+ return { protectedShell: [...storage], shell: [], fileTools: [], readOnly: [...readOnly].sort(), lookup: [...lookup].sort(), homeNames: [] };
313
323
  }
314
324
  /**
315
325
  * Git needs this worktree's metadata and the shared objects/refs, but must not prune or rewrite the metadata of another
@@ -765,6 +775,18 @@ function codexFilesystem(fence, fullAccess, grants) {
765
775
  if (basename(path) === INSTANCE_DIR)
766
776
  access.set(path, "deny");
767
777
  }
778
+ // Protected storage: denied by name and real path, and nothing granted inside it (Codex lets the more specific path
779
+ // win, so a grant there would reopen it).
780
+ const storage = grants.protectedStorage ?? [];
781
+ if (storage.length > 0) {
782
+ for (const [path, mode] of [...access])
783
+ if (mode !== "deny" && storage.some(one => path === one || inside(path, one)))
784
+ access.delete(path);
785
+ for (const path of storage) {
786
+ access.set(path, "deny");
787
+ access.set(real(path), "deny");
788
+ }
789
+ }
768
790
  return access;
769
791
  }
770
792
  /**
@@ -971,6 +993,16 @@ export function srtTermsOf(fence, grants = NO_GRANTS) {
971
993
  denyWrite.add(sealed);
972
994
  }
973
995
  }
996
+ // Protected storage last: denied to read and write by name and real path, and no read grant inside it survives.
997
+ const storage = grants.protectedStorage ?? [];
998
+ const isProtected = (path) => storage.some(one => path === one || inside(path, one) || real(path) === one || inside(real(path), one));
999
+ for (const path of storage) {
1000
+ denyRead.add(path);
1001
+ denyWrite.add(path);
1002
+ }
1003
+ for (const path of [...allowRead])
1004
+ if (isProtected(path))
1005
+ allowRead.delete(path);
974
1006
  const readConfig = { denyOnly: [...denyRead].sort(), allowWithinDeny: [...allowRead].sort() };
975
1007
  const writes = grants.writes;
976
1008
  // A sign-ins check also writes Codex's session and log folders and its own Claude transcripts (never inside a
@@ -983,7 +1015,7 @@ export function srtTermsOf(fence, grants = NO_GRANTS) {
983
1015
  const network = grants.network;
984
1016
  return {
985
1017
  readConfig,
986
- writeConfig: { allowOnly: [...new Set(allowOnly)].sort(), denyWithinAllow: [...denyWrite].sort() },
1018
+ writeConfig: { allowOnly: [...new Set(allowOnly)].filter(path => path === "/" || !isProtected(path.replace(/\*$/, ""))).sort(), denyWithinAllow: [...denyWrite].sort() },
987
1019
  allowMachLookup: [...grants.machLookup ?? [], ...grants.signIns?.mach ?? []],
988
1020
  ...(grants.browser === true ? { browser: true } : {}),
989
1021
  ...(grants.keychain === true ? { keychain: true } : {}),
@@ -1078,7 +1110,9 @@ export function launchSandboxOf(profile) {
1078
1110
  export function fenceFor(kind, ctx) {
1079
1111
  const home = ctx.home === undefined ? {} : { home: ctx.home };
1080
1112
  // The project's read denies join the fence by name, there or not (the runtime denies a path that isn't there yet).
1081
- const fence = agentFence({ databaseFile: ctx.databaseFile, worktree: ctx.worktree, ...home, boundary: ctx.boundary, binding: ctx.binding, runFolders: ctx.runFolders, extra: [...ctx.extra ?? [], ...ctx.readDeny ?? []] });
1113
+ // Protected storage, resolved once for this launch, from the plane's own environment (protected-storage.ts).
1114
+ const storage = protectedStorageDenies(ctx.home ?? homedir());
1115
+ const fence = agentFence({ databaseFile: ctx.databaseFile, worktree: ctx.worktree, ...home, boundary: ctx.boundary, binding: ctx.binding, runFolders: ctx.runFolders, extra: [...ctx.extra ?? [], ...ctx.readDeny ?? []], protectedStorage: storage });
1082
1116
  const found = fenceGrants({ databaseFile: ctx.databaseFile, worktree: ctx.worktree, boundary: ctx.boundary, runFolders: ctx.runFolders, linked: ctx.linked ?? null, binding: ctx.binding, ...home, poolRoots: ctx.poolRoots });
1083
1117
  // Toolroll's own install, the sandbox runtime's package and the Node running them, never written by anything it
1084
1118
  // launches: rewriting the code that fences a build would switch the fence off.
@@ -1095,6 +1129,7 @@ export function fenceFor(kind, ctx) {
1095
1129
  ...(kind === "build" ? { keychain: true } : {}),
1096
1130
  // Under a write allowlist these are out of reach already; where the launch writes anywhere, they are denied by name.
1097
1131
  writeDenies: homeWriteDenies(ctx.home ?? homedir(), ctx.binding, ctx.provider ?? null),
1132
+ protectedStorage: storage,
1098
1133
  ...(made.pool === undefined ? {} : { pool: { ...made.pool, ...(ctx.policy === 2 ? { readable: true } : {}),
1099
1134
  // Flows' scratch folders in the pool (and any the caller names) stay denied whole, at every policy version.
1100
1135
  sealed: sealedScratch(ctx.poolRoots, ctx.scratchRoots ?? []) } }),