toolroll 0.9.69 → 0.9.70
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 +47 -0
- package/README.md +43 -17
- package/THIRD_PARTY_NOTICES.md +21 -1
- package/dist/agent-fence.d.ts +180 -115
- package/dist/agent-fence.js +413 -535
- package/dist/agentconfig.js +6 -1
- package/dist/browser/THIRD_PARTY_NOTICES.txt +21 -0
- package/dist/builder.js +24 -4
- package/dist/check-access.d.ts +6 -9
- package/dist/check-access.js +7 -10
- package/dist/coding-provider.d.ts +14 -0
- package/dist/coding-provider.js +33 -1
- package/dist/coding-workspace.d.ts +15 -0
- package/dist/coding-workspace.js +9 -2
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +4 -0
- package/dist/egress.d.ts +2 -0
- package/dist/egress.js +13 -4
- package/dist/exec.d.ts +17 -3
- package/dist/exec.js +54 -17
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +3 -2
- package/dist/flow-code.d.ts +31 -2
- package/dist/flow-code.js +57 -5
- package/dist/flow-steps.d.ts +4 -0
- package/dist/flow-steps.js +9 -5
- package/dist/flow-triggers.d.ts +4 -0
- package/dist/flow-triggers.js +8 -3
- package/dist/invoke.d.ts +18 -8
- package/dist/invoke.js +98 -115
- package/dist/operate.js +46 -0
- package/dist/project-delete-ui.d.ts +7 -6
- package/dist/project-delete-ui.js +36 -2
- package/dist/project-sandbox.d.ts +59 -0
- package/dist/project-sandbox.js +83 -0
- package/dist/project-tools.d.ts +29 -5
- package/dist/project-tools.js +43 -7
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -0
- package/dist/sandbox-launch.d.ts +1 -0
- package/dist/sandbox-launch.js +148 -0
- package/dist/sandbox-runtime.d.ts +62 -0
- package/dist/sandbox-runtime.js +140 -0
- package/dist/scope.d.ts +28 -0
- package/dist/scope.js +25 -5
- package/dist/scout-browser-lock.d.ts +69 -0
- package/dist/scout-browser-lock.js +69 -0
- package/dist/scout-browser.d.ts +60 -1
- package/dist/scout-browser.js +200 -4
- package/dist/scout.d.ts +9 -4
- package/dist/scout.js +14 -4
- package/dist/serve.js +3 -1
- package/dist/server/render-tasks.js +4 -1
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +27 -0
- package/dist/store.d.ts +31 -1
- package/dist/store.js +83 -4
- package/dist/subagent-tools.d.ts +1 -0
- package/dist/subagent-tools.js +7 -5
- package/dist/tool-broker.d.ts +2 -1
- package/dist/tool-broker.js +2 -2
- package/dist/tool-launcher.js +10 -16
- package/package.json +2 -1
- package/scripts/provider-canary.mjs +5 -6
- package/dist/egress-bridge.d.ts +0 -17
- package/dist/egress-bridge.js +0 -111
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox engine: Anthropic's sandbox-runtime (`@anthropic-ai/sandbox-runtime`, pinned in package.json). Every
|
|
3
|
+
* macOS Seatbelt profile and every Linux bubblewrap + seccomp invocation a fenced launch runs under is generated by
|
|
4
|
+
* its own code, per launch, from a small static config (read denies with exact re-allows, write roots with denies
|
|
5
|
+
* inside them, mach services, the run's proxy). Toolroll adds only what that config says (agent-fence.ts srtTermsOf);
|
|
6
|
+
* the rules themselves, their baseline and their limits are the sandbox runtime's.
|
|
7
|
+
*
|
|
8
|
+
* macOS: its profile builder runs in this process (it is synchronous), with this process's working folder set to the
|
|
9
|
+
* launch's for the call, because the runtime anchors its mandatory write denies (.git/hooks, .vscode, shell profiles)
|
|
10
|
+
* at the working folder. Linux: its builder is asynchronous, anchored at the working folder too, and leaves mount
|
|
11
|
+
* points to clean up after the command, so each Linux launch runs through a small helper process
|
|
12
|
+
* (sandbox-launch.ts) that changes into the launch's folder, asks the runtime for the command, runs it and cleans up.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { wrapCommandWithSandboxMacOS } from "@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js";
|
|
20
|
+
import { checkLinuxDependencies, getLinuxDependencyStatus } from "@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.js";
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
/** The engine and its exact version, as the console and the words say it. */
|
|
23
|
+
export const SANDBOX_RUNTIME = (() => {
|
|
24
|
+
const name = "@anthropic-ai/sandbox-runtime";
|
|
25
|
+
try {
|
|
26
|
+
const manifest = JSON.parse(readFileSync(join(dirname(require.resolve(name)), "..", "package.json"), "utf8"));
|
|
27
|
+
return { name, version: typeof manifest.version === "string" ? manifest.version : "unknown" };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { name, version: "unknown" };
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
/** "Anthropic's sandbox-runtime 0.0.79": how every surface names the engine. */
|
|
34
|
+
export const sandboxRuntimeWords = () => `Anthropic's sandbox-runtime ${SANDBOX_RUNTIME.version}`;
|
|
35
|
+
const SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
36
|
+
/** A deny-by-default profile, as the runtime's are: inside another sandbox one can't be applied (an allow-by-default one
|
|
37
|
+
* still can, so it proves nothing). */
|
|
38
|
+
export const SEATBELT_PROBE = "(version 1)(deny default)(allow process-exec)(allow process-fork)(allow file-read*)(allow sysctl-read)(allow mach-lookup)";
|
|
39
|
+
/** Why the runtime can't sandbox here, or null when it can (checked before every launch; nothing runs bare). */
|
|
40
|
+
export function sandboxRuntimeProblem(platform = process.platform) {
|
|
41
|
+
if (platform === "darwin") {
|
|
42
|
+
if (!existsSync(SANDBOX_EXEC))
|
|
43
|
+
return "this Mac has no sandbox-exec";
|
|
44
|
+
const probe = spawnSync(SANDBOX_EXEC, ["-p", SEATBELT_PROBE, "/usr/bin/true"], { timeout: 5_000, stdio: "ignore" });
|
|
45
|
+
return probe.status === 0 ? null : "sandbox-exec can't start a sandbox here (is this already inside one?)";
|
|
46
|
+
}
|
|
47
|
+
if (platform === "linux") {
|
|
48
|
+
const checked = checkLinuxDependencies();
|
|
49
|
+
if (checked.errors.length > 0)
|
|
50
|
+
return checked.errors.join("; ");
|
|
51
|
+
if (!getLinuxDependencyStatus().hasSeccompApply)
|
|
52
|
+
return "the sandbox runtime's seccomp helper isn't available for this processor";
|
|
53
|
+
// The runtime finds nested protected names (a subfolder's .git/hooks, .vscode, shell profiles) with ripgrep.
|
|
54
|
+
if (spawnSync("rg", ["--version"], { timeout: 5_000, stdio: "ignore" }).status !== 0)
|
|
55
|
+
return "ripgrep (rg) is not installed";
|
|
56
|
+
const probe = spawnSync("bwrap", ["--ro-bind", "/", "/", "--unshare-pid", "--unshare-user", "--proc", "/proc", "--new-session", "--", "true"], { timeout: 5_000, stdio: "ignore" });
|
|
57
|
+
return probe.status === 0 ? null : "bubblewrap can't start a sandbox here (unprivileged user namespaces may be off)";
|
|
58
|
+
}
|
|
59
|
+
return `the sandbox runtime has no sandbox for ${platform}`;
|
|
60
|
+
}
|
|
61
|
+
/** POSIX shell words. */
|
|
62
|
+
export function shellQuote(words) {
|
|
63
|
+
return words.map(word => /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The runtime sets a sandboxed command's TMPDIR itself (to $CLAUDE_CODE_TMPDIR of the process asking, or /tmp/claude):
|
|
67
|
+
* the launch's own temp folder when it has one, so it keeps it; otherwise /tmp/claude, made if missing.
|
|
68
|
+
*/
|
|
69
|
+
export function withRuntimeTemp(temp, make) {
|
|
70
|
+
const before = { code: process.env["CLAUDE_CODE_TMPDIR"], old: process.env["CLAUDE_TMPDIR"] };
|
|
71
|
+
delete process.env["CLAUDE_TMPDIR"];
|
|
72
|
+
if (temp !== undefined && temp !== "")
|
|
73
|
+
process.env["CLAUDE_CODE_TMPDIR"] = temp;
|
|
74
|
+
else {
|
|
75
|
+
delete process.env["CLAUDE_CODE_TMPDIR"];
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync("/tmp/claude", { recursive: true, mode: 0o700 });
|
|
78
|
+
}
|
|
79
|
+
catch { /* someone else's: writes there fail */ }
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return make();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (before.code === undefined)
|
|
86
|
+
delete process.env["CLAUDE_CODE_TMPDIR"];
|
|
87
|
+
else
|
|
88
|
+
process.env["CLAUDE_CODE_TMPDIR"] = before.code;
|
|
89
|
+
if (before.old !== undefined)
|
|
90
|
+
process.env["CLAUDE_TMPDIR"] = before.old;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The runtime's macOS invocation for `file args` run from `cwd`: a command line for /bin/sh. */
|
|
94
|
+
export function srtMacosCommand(file, args, terms, cwd = process.cwd(), temp) {
|
|
95
|
+
const network = terms.network;
|
|
96
|
+
const params = {
|
|
97
|
+
command: shellQuote([file, ...args]),
|
|
98
|
+
needsNetworkRestriction: network.kind === "proxy",
|
|
99
|
+
...(network.kind === "proxy" ? {
|
|
100
|
+
httpProxyPort: network.httpPort,
|
|
101
|
+
...(network.socksPort === undefined ? {} : { socksProxyPort: network.socksPort }),
|
|
102
|
+
...(network.token === undefined ? {} : { proxyAuthToken: network.token }),
|
|
103
|
+
allowLocalBinding: network.localServices,
|
|
104
|
+
...(network.unixSockets.length === 0 ? {} : { allowUnixSockets: network.unixSockets }),
|
|
105
|
+
} : {}),
|
|
106
|
+
...(terms.allowMachLookup.length === 0 ? {} : { allowMachLookup: terms.allowMachLookup }),
|
|
107
|
+
readConfig: terms.readConfig,
|
|
108
|
+
writeConfig: terms.writeConfig,
|
|
109
|
+
};
|
|
110
|
+
// The runtime anchors its mandatory write denies at this process's working folder: the launch's, for this call.
|
|
111
|
+
const before = process.cwd();
|
|
112
|
+
process.chdir(cwd);
|
|
113
|
+
try {
|
|
114
|
+
return withRuntimeTemp(temp, () => wrapCommandWithSandboxMacOS(params));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
process.chdir(before);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The Linux helper (sandbox-launch.ts) and the environment variable its launch is handed in. */
|
|
121
|
+
export const SANDBOX_LAUNCH_SPEC = "TOOLROLL_SANDBOX_LAUNCH";
|
|
122
|
+
export const sandboxLaunchHelper = () => {
|
|
123
|
+
const beside = fileURLToPath(new URL("./sandbox-launch.js", import.meta.url));
|
|
124
|
+
// Run from the sources (the test suite), the built helper is the package's own.
|
|
125
|
+
return existsSync(beside) ? beside : fileURLToPath(new URL("../dist/sandbox-launch.js", import.meta.url));
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* `file args` inside the runtime's sandbox, as a spawn: on macOS `/bin/sh -c <its command>`; on Linux the helper,
|
|
129
|
+
* handed the launch in its environment (never its arguments, which other users can list).
|
|
130
|
+
*/
|
|
131
|
+
export function srtSpawn(file, args, terms, cwd = process.cwd(), temp, platform = process.platform) {
|
|
132
|
+
if (platform === "darwin") {
|
|
133
|
+
const command = srtMacosCommand(file, args, terms, cwd, temp);
|
|
134
|
+
if (!command.includes(SANDBOX_EXEC))
|
|
135
|
+
throw new Error("the sandbox runtime returned the command unsandboxed");
|
|
136
|
+
return { file: "/bin/sh", args: ["-c", command], env: {} };
|
|
137
|
+
}
|
|
138
|
+
const spec = { file, args: [...args], cwd, terms, ...(temp === undefined ? {} : { temp }) };
|
|
139
|
+
return { file: process.execPath, args: [sandboxLaunchHelper()], env: { [SANDBOX_LAUNCH_SPEC]: JSON.stringify(spec) } };
|
|
140
|
+
}
|
package/dist/scope.d.ts
CHANGED
|
@@ -81,6 +81,13 @@ export type ClaudeProfile = {
|
|
|
81
81
|
writes?: WritesMode;
|
|
82
82
|
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
83
83
|
checkAccess?: Exclude<CheckAccess, "sandboxed">;
|
|
84
|
+
/** The fence policy it was sealed under (agent-fence.ts FencePolicy): 2 from 0.9.70 (the home folder read as in
|
|
85
|
+
* Anthropic's sandbox runtime, less `readDeny`); absent on approvals sealed before, which keep v1's home denies. */
|
|
86
|
+
fencePolicy?: 2;
|
|
87
|
+
/** What its builds and checks may not read (project-sandbox.ts), sealed when the project lists any. */
|
|
88
|
+
readDeny?: string[];
|
|
89
|
+
/** macOS services its builds may also reach (project-sandbox.ts), sealed when the project lists any. */
|
|
90
|
+
machLookup?: string[];
|
|
84
91
|
};
|
|
85
92
|
export type CodexShapedProfile = {
|
|
86
93
|
provider: "codex" | "openrouter";
|
|
@@ -103,6 +110,13 @@ export type CodexShapedProfile = {
|
|
|
103
110
|
writes?: WritesMode;
|
|
104
111
|
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
105
112
|
checkAccess?: Exclude<CheckAccess, "sandboxed">;
|
|
113
|
+
/** The fence policy it was sealed under (agent-fence.ts FencePolicy): 2 from 0.9.70 (the home folder read as in
|
|
114
|
+
* Anthropic's sandbox runtime, less `readDeny`); absent on approvals sealed before, which keep v1's home denies. */
|
|
115
|
+
fencePolicy?: 2;
|
|
116
|
+
/** What its builds and checks may not read (project-sandbox.ts), sealed when the project lists any. */
|
|
117
|
+
readDeny?: string[];
|
|
118
|
+
/** macOS services its builds may also reach (project-sandbox.ts), sealed when the project lists any. */
|
|
119
|
+
machLookup?: string[];
|
|
106
120
|
};
|
|
107
121
|
export type GeminiProfile = {
|
|
108
122
|
provider: "gemini";
|
|
@@ -127,6 +141,13 @@ export type GeminiProfile = {
|
|
|
127
141
|
writes?: WritesMode;
|
|
128
142
|
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
129
143
|
checkAccess?: Exclude<CheckAccess, "sandboxed">;
|
|
144
|
+
/** The fence policy it was sealed under (agent-fence.ts FencePolicy): 2 from 0.9.70 (the home folder read as in
|
|
145
|
+
* Anthropic's sandbox runtime, less `readDeny`); absent on approvals sealed before, which keep v1's home denies. */
|
|
146
|
+
fencePolicy?: 2;
|
|
147
|
+
/** What its builds and checks may not read (project-sandbox.ts), sealed when the project lists any. */
|
|
148
|
+
readDeny?: string[];
|
|
149
|
+
/** macOS services its builds may also reach (project-sandbox.ts), sealed when the project lists any. */
|
|
150
|
+
machLookup?: string[];
|
|
130
151
|
};
|
|
131
152
|
/**
|
|
132
153
|
* An agent over the Agent Client Protocol (acp-agents.ts, docs/plans/acp-adapter.md).
|
|
@@ -152,6 +173,13 @@ export type AcpProfile = {
|
|
|
152
173
|
writes?: WritesMode;
|
|
153
174
|
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
154
175
|
checkAccess?: Exclude<CheckAccess, "sandboxed">;
|
|
176
|
+
/** The fence policy it was sealed under (agent-fence.ts FencePolicy): 2 from 0.9.70 (the home folder read as in
|
|
177
|
+
* Anthropic's sandbox runtime, less `readDeny`); absent on approvals sealed before, which keep v1's home denies. */
|
|
178
|
+
fencePolicy?: 2;
|
|
179
|
+
/** What its builds and checks may not read (project-sandbox.ts), sealed when the project lists any. */
|
|
180
|
+
readDeny?: string[];
|
|
181
|
+
/** macOS services its builds may also reach (project-sandbox.ts), sealed when the project lists any. */
|
|
182
|
+
machLookup?: string[];
|
|
155
183
|
};
|
|
156
184
|
export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile | AcpProfile;
|
|
157
185
|
/** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
|
package/dist/scope.js
CHANGED
|
@@ -42,6 +42,7 @@ import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypt
|
|
|
42
42
|
import { networkFromValue, networkWords } from "./egress.js";
|
|
43
43
|
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
44
44
|
import { checkAccessWords, isCheckAccess } from "./check-access.js";
|
|
45
|
+
import { MAX_MACH_LOOKUP, MAX_READ_DENY, sandboxTermsWords, validMachLookup, validReadDeny } from "./project-sandbox.js";
|
|
45
46
|
import { z } from "zod";
|
|
46
47
|
import {} from "./contracts/acceptance-terms.js";
|
|
47
48
|
import { parseContract } from "./contracts/contract.js";
|
|
@@ -360,9 +361,26 @@ export function profileFromJson(json) {
|
|
|
360
361
|
if (sealedAccess !== undefined && (sealedAccess === "sandboxed" || !isCheckAccess(sealedAccess)))
|
|
361
362
|
return null;
|
|
362
363
|
const checkAccess = sealedAccess;
|
|
363
|
-
|
|
364
|
+
// Sandbox parity (v133): the policy version is exactly 2 when present; the lists, when present, are non-empty, sorted,
|
|
365
|
+
// distinct and valid (project-sandbox.ts), as only this code writes them.
|
|
366
|
+
const policy = p["fencePolicy"];
|
|
367
|
+
if (policy !== undefined && policy !== 2)
|
|
368
|
+
return null;
|
|
369
|
+
const sealedList = (key, valid, max) => {
|
|
370
|
+
const list = p[key];
|
|
371
|
+
if (list === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
return Array.isArray(list) && list.length > 0 && list.length <= max && list.every(one => typeof one === "string" && valid(one))
|
|
374
|
+
&& list.every((one, at) => at === 0 || list[at - 1] < one) ? [...list] : null;
|
|
375
|
+
};
|
|
376
|
+
const readDeny = sealedList("readDeny", validReadDeny, MAX_READ_DENY);
|
|
377
|
+
const machLookup = sealedList("machLookup", validMachLookup, MAX_MACH_LOOKUP);
|
|
378
|
+
if (readDeny === null || machLookup === null)
|
|
379
|
+
return null;
|
|
380
|
+
const withNetwork = { ...(network === undefined ? {} : { network }), ...(writes === undefined ? {} : { writes }), ...(checkAccess === undefined ? {} : { checkAccess }),
|
|
381
|
+
...(policy === undefined ? {} : { fencePolicy: 2 }), ...(readDeny === undefined ? {} : { readDeny }), ...(machLookup === undefined ? {} : { machLookup }) };
|
|
364
382
|
if (p["provider"] === "claude") {
|
|
365
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
|
|
383
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
366
384
|
str(p["model"]) &&
|
|
367
385
|
(p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
|
|
368
386
|
num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
|
|
@@ -384,7 +402,7 @@ export function profileFromJson(json) {
|
|
|
384
402
|
return null;
|
|
385
403
|
}
|
|
386
404
|
if (p["provider"] === "codex" || p["provider"] === "openrouter") {
|
|
387
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
|
|
405
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
388
406
|
str(p["model"]) &&
|
|
389
407
|
(p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
|
|
390
408
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -406,7 +424,7 @@ export function profileFromJson(json) {
|
|
|
406
424
|
return null;
|
|
407
425
|
}
|
|
408
426
|
if (p["provider"] === "gemini") {
|
|
409
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
|
|
427
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
410
428
|
str(p["model"]) &&
|
|
411
429
|
(p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
|
|
412
430
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -428,7 +446,7 @@ export function profileFromJson(json) {
|
|
|
428
446
|
return null;
|
|
429
447
|
}
|
|
430
448
|
if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
|
|
431
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes", "checkAccess"]) &&
|
|
449
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
432
450
|
str(p["model"]) &&
|
|
433
451
|
p["permission"] === "full" &&
|
|
434
452
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -1278,6 +1296,8 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1278
1296
|
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1279
1297
|
...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
|
|
1280
1298
|
...(scope.profile?.checkAccess === undefined ? [] : [` checks ${checkAccessWords(scope.profile.checkAccess)}`]),
|
|
1299
|
+
// Its sandbox terms (project-sandbox.ts), part of what the yes agrees to: what it reads, and any widening.
|
|
1300
|
+
...(scope.profile === null || scope.profile === undefined ? [] : ((terms) => [` reads ${terms.reads}`, ...(terms.mach === null ? [] : [` services ${terms.mach}`])])(sandboxTermsWords(scope.profile))),
|
|
1281
1301
|
` reference ${scope.digest}`,
|
|
1282
1302
|
` approved ${describeApproval(approval)}`,
|
|
1283
1303
|
];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scout browser helper's pinned install (scout-browser.ts ensureScoutBrowserHelper): its manifest and lockfile,
|
|
3
|
+
* generated once with `npm install --package-lock-only --registry https://registry.npmjs.org/` for @playwright/mcp
|
|
4
|
+
* 0.0.82. `npm ci` installs exactly this tree and refuses any tarball whose sha512 differs. Changing a version here is
|
|
5
|
+
* a supply-chain change: regenerate both and review the diff.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SCOUT_BROWSER_MANIFEST: {
|
|
8
|
+
readonly name: "toolroll-scout-browser";
|
|
9
|
+
readonly private: true;
|
|
10
|
+
readonly dependencies: {
|
|
11
|
+
readonly "@playwright/mcp": "0.0.82";
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare const SCOUT_BROWSER_LOCK: {
|
|
15
|
+
readonly name: "toolroll-scout-browser";
|
|
16
|
+
readonly lockfileVersion: 3;
|
|
17
|
+
readonly requires: true;
|
|
18
|
+
readonly packages: {
|
|
19
|
+
readonly "": {
|
|
20
|
+
readonly name: "toolroll-scout-browser";
|
|
21
|
+
readonly dependencies: {
|
|
22
|
+
readonly "@playwright/mcp": "0.0.82";
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
readonly "node_modules/@playwright/mcp": {
|
|
26
|
+
readonly version: "0.0.82";
|
|
27
|
+
readonly resolved: "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.82.tgz";
|
|
28
|
+
readonly integrity: "sha512-OCqftfb8H4dnqm/njbTBRk3seUvUPttOlJUxCtEzXGETYOlRH5Qt3bbXIjmZIuWAxD9RF+yg1ASrPeXvm0y5cA==";
|
|
29
|
+
readonly license: "Apache-2.0";
|
|
30
|
+
readonly dependencies: {
|
|
31
|
+
readonly playwright: "1.64.0-alpha-1789764292000";
|
|
32
|
+
readonly "playwright-core": "1.64.0-alpha-1789764292000";
|
|
33
|
+
};
|
|
34
|
+
readonly bin: {
|
|
35
|
+
readonly "playwright-mcp": "cli.js";
|
|
36
|
+
};
|
|
37
|
+
readonly engines: {
|
|
38
|
+
readonly node: ">=18";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
readonly "node_modules/playwright": {
|
|
42
|
+
readonly version: "1.64.0-alpha-1789764292000";
|
|
43
|
+
readonly resolved: "https://registry.npmjs.org/playwright/-/playwright-1.64.0-alpha-1789764292000.tgz";
|
|
44
|
+
readonly integrity: "sha512-3Ngs4ERGdC912uW3srEDtNVey4u1wSaQ/EFcKhxMpz0by0K6nidaJgM087pLpJc1osytiVnL7ack5gvgPArPNw==";
|
|
45
|
+
readonly license: "Apache-2.0";
|
|
46
|
+
readonly dependencies: {
|
|
47
|
+
readonly "playwright-core": "1.64.0-alpha-1789764292000";
|
|
48
|
+
};
|
|
49
|
+
readonly bin: {
|
|
50
|
+
readonly playwright: "cli.js";
|
|
51
|
+
};
|
|
52
|
+
readonly engines: {
|
|
53
|
+
readonly node: ">=20";
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
readonly "node_modules/playwright-core": {
|
|
57
|
+
readonly version: "1.64.0-alpha-1789764292000";
|
|
58
|
+
readonly resolved: "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0-alpha-1789764292000.tgz";
|
|
59
|
+
readonly integrity: "sha512-ZgRaybFv4rRy7QMGnYprEGFdNJnvapNqcaER2w96bDQA+40BWIle1el1Yh9KHD3E6XYmieMB+r+UxFTCauTGGQ==";
|
|
60
|
+
readonly license: "Apache-2.0";
|
|
61
|
+
readonly bin: {
|
|
62
|
+
readonly "playwright-core": "cli.js";
|
|
63
|
+
};
|
|
64
|
+
readonly engines: {
|
|
65
|
+
readonly node: ">=20";
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scout browser helper's pinned install (scout-browser.ts ensureScoutBrowserHelper): its manifest and lockfile,
|
|
3
|
+
* generated once with `npm install --package-lock-only --registry https://registry.npmjs.org/` for @playwright/mcp
|
|
4
|
+
* 0.0.82. `npm ci` installs exactly this tree and refuses any tarball whose sha512 differs. Changing a version here is
|
|
5
|
+
* a supply-chain change: regenerate both and review the diff.
|
|
6
|
+
*/
|
|
7
|
+
export const SCOUT_BROWSER_MANIFEST = {
|
|
8
|
+
"name": "toolroll-scout-browser",
|
|
9
|
+
"private": true,
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@playwright/mcp": "0.0.82"
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const SCOUT_BROWSER_LOCK = {
|
|
15
|
+
"name": "toolroll-scout-browser",
|
|
16
|
+
"lockfileVersion": 3,
|
|
17
|
+
"requires": true,
|
|
18
|
+
"packages": {
|
|
19
|
+
"": {
|
|
20
|
+
"name": "toolroll-scout-browser",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@playwright/mcp": "0.0.82"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"node_modules/@playwright/mcp": {
|
|
26
|
+
"version": "0.0.82",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.82.tgz",
|
|
28
|
+
"integrity": "sha512-OCqftfb8H4dnqm/njbTBRk3seUvUPttOlJUxCtEzXGETYOlRH5Qt3bbXIjmZIuWAxD9RF+yg1ASrPeXvm0y5cA==",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"playwright": "1.64.0-alpha-1789764292000",
|
|
32
|
+
"playwright-core": "1.64.0-alpha-1789764292000"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"playwright-mcp": "cli.js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"node_modules/playwright": {
|
|
42
|
+
"version": "1.64.0-alpha-1789764292000",
|
|
43
|
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0-alpha-1789764292000.tgz",
|
|
44
|
+
"integrity": "sha512-3Ngs4ERGdC912uW3srEDtNVey4u1wSaQ/EFcKhxMpz0by0K6nidaJgM087pLpJc1osytiVnL7ack5gvgPArPNw==",
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"playwright-core": "1.64.0-alpha-1789764292000"
|
|
48
|
+
},
|
|
49
|
+
"bin": {
|
|
50
|
+
"playwright": "cli.js"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"node_modules/playwright-core": {
|
|
57
|
+
"version": "1.64.0-alpha-1789764292000",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0-alpha-1789764292000.tgz",
|
|
59
|
+
"integrity": "sha512-ZgRaybFv4rRy7QMGnYprEGFdNJnvapNqcaER2w96bDQA+40BWIle1el1Yh9KHD3E6XYmieMB+r+UxFTCauTGGQ==",
|
|
60
|
+
"license": "Apache-2.0",
|
|
61
|
+
"bin": {
|
|
62
|
+
"playwright-core": "cli.js"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
package/dist/scout-browser.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export type BrowserStarter = (launch: BrowserLaunch, options: BrowserOptions) =>
|
|
|
17
17
|
export type BrowserOptions = {
|
|
18
18
|
/** The browser tool's environment: the scout's own, its proxy settings included. */
|
|
19
19
|
env: Record<string, string | undefined>;
|
|
20
|
+
/** Toolroll's helpers folder (in its state folder) the pinned browser is installed in, and the scout's proxy: the
|
|
21
|
+
* launch contract's trusted inputs, from the plane, never from the launch. */
|
|
22
|
+
contract?: {
|
|
23
|
+
helpers: string;
|
|
24
|
+
proxy: string;
|
|
25
|
+
};
|
|
20
26
|
/** Where it saves screenshots (its `--output-dir`), also its working folder. */
|
|
21
27
|
folder: string;
|
|
22
28
|
/** The whole start and check, from spawn to screenshot (review 828: at most 60 seconds). */
|
|
@@ -24,7 +30,8 @@ export type BrowserOptions = {
|
|
|
24
30
|
};
|
|
25
31
|
/** The public page the check opens through the scout's proxy. */
|
|
26
32
|
export declare const PREFLIGHT_PAGE = "https://example.com/";
|
|
27
|
-
/** How a scout gets its browser: started for real, or, for a test's stub agent, a placeholder
|
|
33
|
+
/** How a scout gets its browser: started for real under its launch contract, or, for a test's stub agent, a placeholder
|
|
34
|
+
* that starts nothing. */
|
|
28
35
|
export declare function starterFor(agentInjected: boolean): BrowserStarter;
|
|
29
36
|
/** Start the browser tool outside the fence and check it opens a public page and saves a screenshot. */
|
|
30
37
|
export declare function startScoutBrowser(launch: BrowserLaunch, options: BrowserOptions): Promise<BrowserStart>;
|
|
@@ -46,3 +53,55 @@ export type McpSession = {
|
|
|
46
53
|
};
|
|
47
54
|
/** A client of the browser's HTTP transport, as the scout's own is: every step answers by `deadline`, or says why not. */
|
|
48
55
|
export declare function openSession(url: string, deadline: number, late: string): McpSession;
|
|
56
|
+
/** The pinned browser tool, installed once into Toolroll's helpers folder. */
|
|
57
|
+
export declare const SCOUT_BROWSER_PACKAGE = "@playwright/mcp@0.0.82";
|
|
58
|
+
/** The names of the worker's environment the browser tool gets: an allowlist, never a denylist. */
|
|
59
|
+
export declare const SCOUT_BROWSER_ENV: readonly string[];
|
|
60
|
+
/** What the run records of the exception it ran under. */
|
|
61
|
+
export declare const SCOUT_BROWSER_CONTAINMENT = "Chrome's sandbox";
|
|
62
|
+
/** Where the pinned browser lives under `helpers`. */
|
|
63
|
+
export declare function scoutBrowserHelper(helpers: string): {
|
|
64
|
+
folder: string;
|
|
65
|
+
cli: string;
|
|
66
|
+
browsers: string;
|
|
67
|
+
};
|
|
68
|
+
/** The scout's browser tool: the pinned helper, run by Toolroll's own Node, headless, on its bundled Chrome. */
|
|
69
|
+
export declare function scoutBrowserTool(helpers: string): BrowserLaunch;
|
|
70
|
+
/**
|
|
71
|
+
* Why a browser launch breaks its contract, or null when it holds. Every input it trusts is the plane's: the helpers
|
|
72
|
+
* folder and the proxy come from `options.contract`, never from the launch. The executable must be Toolroll's own
|
|
73
|
+
* Node; the script the pinned helper's, inside a private helpers folder; the arguments exactly the expected ones (no
|
|
74
|
+
* way to switch Chrome's sandbox off, point at another browser or executable, or drop the proxy); the working folder
|
|
75
|
+
* the image folder; the environment only allowlisted names.
|
|
76
|
+
*/
|
|
77
|
+
export declare function browserContractProblem(launch: BrowserLaunch, options: BrowserOptions): string | null;
|
|
78
|
+
/** The pinned lockfile's digest, recorded when the helper is installed and checked before every use. */
|
|
79
|
+
export declare const SCOUT_BROWSER_LOCK_DIGEST: string;
|
|
80
|
+
/** The npm of the Node running Toolroll (`lib/node_modules/npm/bin/npm-cli.js` beside it), run by that Node by path:
|
|
81
|
+
* never an `npm` found on PATH. Null where this Node has none beside it (a bundled runtime). */
|
|
82
|
+
export declare function ownNpm(execPath?: string): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Install the pinned browser into the helpers folder once. The worker runs this outside every fence, so nothing a build
|
|
85
|
+
* could have written may steer it: npm is Toolroll's own Node's, run by path; HOME is the staging folder and the user
|
|
86
|
+
* and global npm configurations are an empty file there (no ~/.npmrc, no global npmrc, no registry, script or Node
|
|
87
|
+
* option from either); the registry is pinned; install scripts never run; `npm ci` installs exactly the pinned lockfile
|
|
88
|
+
* (scout-browser-lock.ts) and refuses a tarball whose integrity differs; the installed versions are checked against it
|
|
89
|
+
* before ready.json is written. The npm cache and Chrome for Testing stay in the folder, never in the user's caches.
|
|
90
|
+
* Residual: Chrome for Testing comes from Playwright's own download over HTTPS, without a pinned hash.
|
|
91
|
+
*/
|
|
92
|
+
export declare function ensureScoutBrowserHelper(helpers: string, timeoutMs?: number): Promise<{
|
|
93
|
+
ok: true;
|
|
94
|
+
} | {
|
|
95
|
+
ok: false;
|
|
96
|
+
reason: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** How the helper's npm runs in `staging`: Toolroll's own npm by path, with its home, user and global configuration
|
|
99
|
+
* the staging folder's own empty ones, the registry and cache pinned; the environment only these names. */
|
|
100
|
+
export declare function helperNpm(staging: string, npm: string): {
|
|
101
|
+
env: Record<string, string>;
|
|
102
|
+
args: string[];
|
|
103
|
+
};
|
|
104
|
+
/** What of an installed tree differs from the pinned lockfile (a package missing or at another version), or null. */
|
|
105
|
+
export declare function lockDrift(folder: string): string | null;
|
|
106
|
+
/** The production starter: the pinned helper in place, then the launch held to its contract, then started. */
|
|
107
|
+
export declare function startContractedBrowser(launch: BrowserLaunch, options: BrowserOptions): Promise<BrowserStart>;
|