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.
- package/CHANGELOG.md +76 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/browser/workspace.js +28 -28
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +80 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +8 -2
- package/dist/release.js +79 -6
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/settings.js +26 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- 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
|
package/dist/agent-fence.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/agent-fence.js
CHANGED
|
@@ -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
|
-
|
|
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 ?? []) } }),
|