toolroll 0.9.69 → 0.9.71
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 +67 -0
- package/README.md +43 -17
- package/THIRD_PARTY_NOTICES.md +21 -1
- package/dist/agent-fence.d.ts +184 -115
- package/dist/agent-fence.js +418 -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 +8 -11
- 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 +25 -8
- package/dist/invoke.js +115 -115
- package/dist/operate.js +46 -0
- package/dist/project-delete-ui.d.ts +7 -6
- package/dist/project-delete-ui.js +37 -3
- 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 +109 -0
- package/dist/sandbox-runtime.js +281 -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,109 @@
|
|
|
1
|
+
import type { FsReadRestrictionConfig, FsWriteRestrictionConfig } from "@anthropic-ai/sandbox-runtime";
|
|
2
|
+
/** The engine and its exact version, as the console and the words say it. */
|
|
3
|
+
export declare const SANDBOX_RUNTIME: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
/** "Anthropic's sandbox-runtime 0.0.79": how every surface names the engine. */
|
|
8
|
+
export declare const sandboxRuntimeWords: () => string;
|
|
9
|
+
/**
|
|
10
|
+
* One launch's sandbox, as the runtime's own config: what it may not read (and the exact paths read again inside
|
|
11
|
+
* those), where it may write (and what stays unwritable there), the extra macOS services it may look up, and its
|
|
12
|
+
* network (open, or only through this run's proxy; a tool broker rides the runtime's second relay).
|
|
13
|
+
*/
|
|
14
|
+
export type SrtTerms = {
|
|
15
|
+
readConfig: FsReadRestrictionConfig;
|
|
16
|
+
writeConfig: FsWriteRestrictionConfig;
|
|
17
|
+
allowMachLookup: string[];
|
|
18
|
+
/** macOS: the launch may reach the keychain's services (securityd). Only a build's agent and a check with sign-ins:
|
|
19
|
+
* every other launch is denied them (SEATBELT_NO_KEYCHAIN). */
|
|
20
|
+
keychain?: boolean;
|
|
21
|
+
network: {
|
|
22
|
+
kind: "open";
|
|
23
|
+
} | {
|
|
24
|
+
kind: "proxy";
|
|
25
|
+
httpPort: number;
|
|
26
|
+
socksPort?: number;
|
|
27
|
+
token?: string;
|
|
28
|
+
localServices: boolean;
|
|
29
|
+
unixSockets: string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** A deny-by-default profile, as the runtime's are: inside another sandbox one can't be applied (an allow-by-default one
|
|
33
|
+
* still can, so it proves nothing). */
|
|
34
|
+
export declare const SEATBELT_PROBE = "(version 1)(deny default)(allow process-exec)(allow process-fork)(allow file-read*)(allow sysctl-read)(allow mach-lookup)";
|
|
35
|
+
/** Why the runtime can't sandbox here, or null when it can (checked before every launch; nothing runs bare). */
|
|
36
|
+
export declare function sandboxRuntimeProblem(platform?: NodeJS.Platform): string | null;
|
|
37
|
+
/** POSIX shell words. */
|
|
38
|
+
export declare function shellQuote(words: readonly string[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* The runtime sets a sandboxed command's TMPDIR itself (to $CLAUDE_CODE_TMPDIR of the process asking, or /tmp/claude):
|
|
41
|
+
* the launch's own temp folder when it has one, so it keeps it; otherwise /tmp/claude, made if missing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function withRuntimeTemp<T>(temp: string | undefined, make: () => T): T;
|
|
44
|
+
/**
|
|
45
|
+
* Toolroll's own Seatbelt rules, appended to the end of the runtime's profile (Seatbelt takes the last rule that
|
|
46
|
+
* matches). Static: constants here, never made from a launch's data.
|
|
47
|
+
*
|
|
48
|
+
* Terminals: a pseudo-terminal of the launch's own (node-pty, `script`, a test that drives a shell), and no other. The
|
|
49
|
+
* runtime's `allowPty` opens reads and writes on every /dev/ttys device, the operator's own open terminals among them,
|
|
50
|
+
* and its baseline already reads them (a read steals what is typed there). macOS marks a terminal the sandboxed process
|
|
51
|
+
* made with the `com.apple.sandbox.pty` extension (for that terminal only: a device number used again later outside is
|
|
52
|
+
* not covered), so only those are opened by name. A terminal handed down open (a live terminal's own) stays usable.
|
|
53
|
+
*/
|
|
54
|
+
export declare const SEATBELT_PTY: string;
|
|
55
|
+
/** Last in every profile: no other terminal opened by name, and no keystrokes pushed into any terminal (TIOCSTI,
|
|
56
|
+
* _IOW('t', 114, char)), its own controlling terminal included. */
|
|
57
|
+
export declare const SEATBELT_NO_OTHER_TERMINAL: string;
|
|
58
|
+
/**
|
|
59
|
+
* Last in every launch's profile but a build agent's and a check with sign-ins: the keychain's services. The runtime's
|
|
60
|
+
* baseline lets every sandboxed process ask securityd, so it reads the login keychain's items whose access list lets
|
|
61
|
+
* any program read them (anthropics/sandbox-runtime#755). Build agents keep it (Claude Code signs in from the keychain).
|
|
62
|
+
*/
|
|
63
|
+
export declare const SEATBELT_NO_KEYCHAIN: string;
|
|
64
|
+
/** Toolroll's rules for a launch's terms, in order: its own terminal (unless its read denies cover the devices), then
|
|
65
|
+
* the denies that must come last. */
|
|
66
|
+
export declare function seatbeltRulesFor(terms: SrtTerms): string;
|
|
67
|
+
/** The runtime's own quoting (its utils/shell-quote.js): a word bare when nothing in it is special, otherwise in single
|
|
68
|
+
* quotes with a quote inside as '"'"'. */
|
|
69
|
+
export declare function srtQuote(words: readonly string[]): string;
|
|
70
|
+
/** The words of a line srtQuote made, or null for any other line (an operator, a second command, other quoting). */
|
|
71
|
+
export declare function srtWords(line: string): string[] | null;
|
|
72
|
+
/**
|
|
73
|
+
* `command` (the runtime's macOS invocation) with `rules` appended to its profile, made again from its words. The
|
|
74
|
+
* runtime's invocation is exactly `env [-u NAME]… [NAME=value]… /usr/bin/sandbox-exec -p <profile> <shell> -c
|
|
75
|
+
* <command>`, each word quoted its way, and `<command>` is `expected` when given. Any other line (an operator, a word
|
|
76
|
+
* before `env` or after the command, another program) is refused, so the launch doesn't run.
|
|
77
|
+
*/
|
|
78
|
+
export declare function withSeatbeltRules(command: string, rules: string, expected?: string): string;
|
|
79
|
+
/** The runtime's macOS invocation for `file args` run from `cwd`: a command line for /bin/sh. */
|
|
80
|
+
export declare function srtMacosCommand(file: string, args: readonly string[], terms: SrtTerms, cwd?: string, temp?: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* macOS: what every sandboxed launch runs first, outside the sandbox, before it execs the runtime's command: a session of
|
|
83
|
+
* its own (setsid, syscall 147), so no terminal of whoever started Toolroll is its controlling terminal to reach. A
|
|
84
|
+
* launch that is a session leader already (a live terminal's program, its own terminal made for it) keeps that one; any
|
|
85
|
+
* other that still has a controlling terminal (getsid, syscall 310) is refused. Perl with -t reads neither PERL5OPT nor
|
|
86
|
+
* PERL5LIB, and -X keeps its taint warnings off the launch's output.
|
|
87
|
+
*/
|
|
88
|
+
export declare const DETACH: readonly string[];
|
|
89
|
+
/** The Linux helper (sandbox-launch.ts) and the environment variable its launch is handed in. */
|
|
90
|
+
export declare const SANDBOX_LAUNCH_SPEC = "TOOLROLL_SANDBOX_LAUNCH";
|
|
91
|
+
export declare const sandboxLaunchHelper: () => string;
|
|
92
|
+
/** What the Linux helper is given: the command, where it runs, and the terms. */
|
|
93
|
+
export type SandboxLaunchSpec = {
|
|
94
|
+
file: string;
|
|
95
|
+
args: string[];
|
|
96
|
+
cwd: string;
|
|
97
|
+
terms: SrtTerms;
|
|
98
|
+
temp?: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* `file args` inside the runtime's sandbox, as a spawn: on macOS `/bin/sh -c <its command>` in a session of its own
|
|
102
|
+
* (DETACH); on Linux the helper,
|
|
103
|
+
* handed the launch in its environment (never its arguments, which other users can list).
|
|
104
|
+
*/
|
|
105
|
+
export declare function srtSpawn(file: string, args: readonly string[], terms: SrtTerms, cwd?: string, temp?: string, platform?: NodeJS.Platform): {
|
|
106
|
+
file: string;
|
|
107
|
+
args: string[];
|
|
108
|
+
env: Record<string, string>;
|
|
109
|
+
};
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
* plus, on macOS, a few static rules of its own appended to the end of the profile (seatbeltRulesFor: a terminal of the
|
|
7
|
+
* launch's own and no other, no keystrokes pushed into a terminal, the keychain's services only for builds); the rest
|
|
8
|
+
* of the rules, their baseline and their limits are the runtime's.
|
|
9
|
+
*
|
|
10
|
+
* macOS: its profile builder runs in this process (it is synchronous), with this process's working folder set to the
|
|
11
|
+
* launch's for the call, because the runtime anchors its mandatory write denies (.git/hooks, .vscode, shell profiles)
|
|
12
|
+
* at the working folder. Linux: its builder is asynchronous, anchored at the working folder too, and leaves mount
|
|
13
|
+
* points to clean up after the command, so each Linux launch runs through a small helper process
|
|
14
|
+
* (sandbox-launch.ts) that changes into the launch's folder, asks the runtime for the command, runs it and cleans up.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { wrapCommandWithSandboxMacOS } from "@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js";
|
|
22
|
+
import { checkLinuxDependencies, getLinuxDependencyStatus } from "@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.js";
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
/** The engine and its exact version, as the console and the words say it. */
|
|
25
|
+
export const SANDBOX_RUNTIME = (() => {
|
|
26
|
+
const name = "@anthropic-ai/sandbox-runtime";
|
|
27
|
+
try {
|
|
28
|
+
const manifest = JSON.parse(readFileSync(join(dirname(require.resolve(name)), "..", "package.json"), "utf8"));
|
|
29
|
+
return { name, version: typeof manifest.version === "string" ? manifest.version : "unknown" };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { name, version: "unknown" };
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
/** "Anthropic's sandbox-runtime 0.0.79": how every surface names the engine. */
|
|
36
|
+
export const sandboxRuntimeWords = () => `Anthropic's sandbox-runtime ${SANDBOX_RUNTIME.version}`;
|
|
37
|
+
const SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
38
|
+
/** A deny-by-default profile, as the runtime's are: inside another sandbox one can't be applied (an allow-by-default one
|
|
39
|
+
* still can, so it proves nothing). */
|
|
40
|
+
export const SEATBELT_PROBE = "(version 1)(deny default)(allow process-exec)(allow process-fork)(allow file-read*)(allow sysctl-read)(allow mach-lookup)";
|
|
41
|
+
/** Why the runtime can't sandbox here, or null when it can (checked before every launch; nothing runs bare). */
|
|
42
|
+
export function sandboxRuntimeProblem(platform = process.platform) {
|
|
43
|
+
if (platform === "darwin") {
|
|
44
|
+
if (!existsSync(SANDBOX_EXEC))
|
|
45
|
+
return "this Mac has no sandbox-exec";
|
|
46
|
+
const probe = spawnSync(SANDBOX_EXEC, ["-p", SEATBELT_PROBE, "/usr/bin/true"], { timeout: 5_000, stdio: "ignore" });
|
|
47
|
+
return probe.status === 0 ? null : "sandbox-exec can't start a sandbox here (is this already inside one?)";
|
|
48
|
+
}
|
|
49
|
+
if (platform === "linux") {
|
|
50
|
+
const checked = checkLinuxDependencies();
|
|
51
|
+
if (checked.errors.length > 0)
|
|
52
|
+
return checked.errors.join("; ");
|
|
53
|
+
if (!getLinuxDependencyStatus().hasSeccompApply)
|
|
54
|
+
return "the sandbox runtime's seccomp helper isn't available for this processor";
|
|
55
|
+
// The runtime finds nested protected names (a subfolder's .git/hooks, .vscode, shell profiles) with ripgrep.
|
|
56
|
+
if (spawnSync("rg", ["--version"], { timeout: 5_000, stdio: "ignore" }).status !== 0)
|
|
57
|
+
return "ripgrep (rg) is not installed";
|
|
58
|
+
const probe = spawnSync("bwrap", ["--ro-bind", "/", "/", "--unshare-pid", "--unshare-user", "--proc", "/proc", "--new-session", "--", "true"], { timeout: 5_000, stdio: "ignore" });
|
|
59
|
+
return probe.status === 0 ? null : "bubblewrap can't start a sandbox here (unprivileged user namespaces may be off)";
|
|
60
|
+
}
|
|
61
|
+
return `the sandbox runtime has no sandbox for ${platform}`;
|
|
62
|
+
}
|
|
63
|
+
/** POSIX shell words. */
|
|
64
|
+
export function shellQuote(words) {
|
|
65
|
+
return words.map(word => /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The runtime sets a sandboxed command's TMPDIR itself (to $CLAUDE_CODE_TMPDIR of the process asking, or /tmp/claude):
|
|
69
|
+
* the launch's own temp folder when it has one, so it keeps it; otherwise /tmp/claude, made if missing.
|
|
70
|
+
*/
|
|
71
|
+
export function withRuntimeTemp(temp, make) {
|
|
72
|
+
const before = { code: process.env["CLAUDE_CODE_TMPDIR"], old: process.env["CLAUDE_TMPDIR"] };
|
|
73
|
+
delete process.env["CLAUDE_TMPDIR"];
|
|
74
|
+
if (temp !== undefined && temp !== "")
|
|
75
|
+
process.env["CLAUDE_CODE_TMPDIR"] = temp;
|
|
76
|
+
else {
|
|
77
|
+
delete process.env["CLAUDE_CODE_TMPDIR"];
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync("/tmp/claude", { recursive: true, mode: 0o700 });
|
|
80
|
+
}
|
|
81
|
+
catch { /* someone else's: writes there fail */ }
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return make();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (before.code === undefined)
|
|
88
|
+
delete process.env["CLAUDE_CODE_TMPDIR"];
|
|
89
|
+
else
|
|
90
|
+
process.env["CLAUDE_CODE_TMPDIR"] = before.code;
|
|
91
|
+
if (before.old !== undefined)
|
|
92
|
+
process.env["CLAUDE_TMPDIR"] = before.old;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Toolroll's own Seatbelt rules, appended to the end of the runtime's profile (Seatbelt takes the last rule that
|
|
97
|
+
* matches). Static: constants here, never made from a launch's data.
|
|
98
|
+
*
|
|
99
|
+
* Terminals: a pseudo-terminal of the launch's own (node-pty, `script`, a test that drives a shell), and no other. The
|
|
100
|
+
* runtime's `allowPty` opens reads and writes on every /dev/ttys device, the operator's own open terminals among them,
|
|
101
|
+
* and its baseline already reads them (a read steals what is typed there). macOS marks a terminal the sandboxed process
|
|
102
|
+
* made with the `com.apple.sandbox.pty` extension (for that terminal only: a device number used again later outside is
|
|
103
|
+
* not covered), so only those are opened by name. A terminal handed down open (a live terminal's own) stays usable.
|
|
104
|
+
*/
|
|
105
|
+
export const SEATBELT_PTY = [
|
|
106
|
+
"; Toolroll: a pseudo-terminal the launch made itself",
|
|
107
|
+
"(allow pseudo-tty)",
|
|
108
|
+
"(allow file-read* file-write* file-ioctl (literal \"/dev/ptmx\"))",
|
|
109
|
+
"(allow file-read* file-write* file-ioctl (require-all (regex #\"^/dev/ttys[0-9]+$\") (extension \"com.apple.sandbox.pty\")))",
|
|
110
|
+
].join("\n");
|
|
111
|
+
/** Last in every profile: no other terminal opened by name, and no keystrokes pushed into any terminal (TIOCSTI,
|
|
112
|
+
* _IOW('t', 114, char)), its own controlling terminal included. */
|
|
113
|
+
export const SEATBELT_NO_OTHER_TERMINAL = [
|
|
114
|
+
"; Toolroll: no other terminal, and nothing typed into one",
|
|
115
|
+
"(deny file-read-data file-write* (require-all (regex #\"^/dev/ttys[0-9]+$\") (require-not (extension \"com.apple.sandbox.pty\"))))",
|
|
116
|
+
"(deny file-ioctl (ioctl-command 2147578994))",
|
|
117
|
+
].join("\n");
|
|
118
|
+
/**
|
|
119
|
+
* Last in every launch's profile but a build agent's and a check with sign-ins: the keychain's services. The runtime's
|
|
120
|
+
* baseline lets every sandboxed process ask securityd, so it reads the login keychain's items whose access list lets
|
|
121
|
+
* any program read them (anthropics/sandbox-runtime#755). Build agents keep it (Claude Code signs in from the keychain).
|
|
122
|
+
*/
|
|
123
|
+
export const SEATBELT_NO_KEYCHAIN = [
|
|
124
|
+
"; Toolroll: no keychain",
|
|
125
|
+
"(deny mach-lookup (global-name \"com.apple.SecurityServer\") (global-name \"com.apple.securityd.xpc\"))",
|
|
126
|
+
].join("\n");
|
|
127
|
+
/** Whether a read deny covers the launch's own terminal devices (/dev/ptmx, /dev/ttys*): then it gets none. */
|
|
128
|
+
function deniesTerminals(deny) {
|
|
129
|
+
const glob = deny.search(/[*?[]/);
|
|
130
|
+
const path = (glob < 0 ? deny : deny.slice(0, glob)).replace(/\/+$/, "");
|
|
131
|
+
const devices = ["/dev/ptmx", "/dev/ttys0"];
|
|
132
|
+
if (glob >= 0)
|
|
133
|
+
return devices.some(device => device.startsWith(path));
|
|
134
|
+
return path === "" || path === "/dev" || devices.includes(path) || /^\/dev\/ttys[0-9]*$/.test(path);
|
|
135
|
+
}
|
|
136
|
+
/** Toolroll's rules for a launch's terms, in order: its own terminal (unless its read denies cover the devices), then
|
|
137
|
+
* the denies that must come last. */
|
|
138
|
+
export function seatbeltRulesFor(terms) {
|
|
139
|
+
const denied = [...terms.readConfig.denyOnly, ...terms.readConfig.literalDenyOnly ?? []].some(one => deniesTerminals(typeof one === "string" ? one : String(one)));
|
|
140
|
+
return [...denied ? [] : [SEATBELT_PTY], SEATBELT_NO_OTHER_TERMINAL, ...terms.keychain === true ? [] : [SEATBELT_NO_KEYCHAIN]].join("\n");
|
|
141
|
+
}
|
|
142
|
+
const BARE_WORD = /^[A-Za-z0-9_./:@+,-][A-Za-z0-9_./:=@+,-]*$/;
|
|
143
|
+
/** The runtime's own quoting (its utils/shell-quote.js): a word bare when nothing in it is special, otherwise in single
|
|
144
|
+
* quotes with a quote inside as '"'"'. */
|
|
145
|
+
export function srtQuote(words) {
|
|
146
|
+
return words.map(word => word === "" ? "''" : BARE_WORD.test(word) ? word : `'${word.replace(/'/g, `'"'"'`)}'`).join(" ");
|
|
147
|
+
}
|
|
148
|
+
/** The words of a line srtQuote made, or null for any other line (an operator, a second command, other quoting). */
|
|
149
|
+
export function srtWords(line) {
|
|
150
|
+
const words = [];
|
|
151
|
+
let at = 0;
|
|
152
|
+
while (at < line.length) {
|
|
153
|
+
let word = "";
|
|
154
|
+
if (line[at] === "'") {
|
|
155
|
+
for (;;) {
|
|
156
|
+
const close = line.indexOf("'", at + 1);
|
|
157
|
+
if (close < 0)
|
|
158
|
+
return null;
|
|
159
|
+
word += line.slice(at + 1, close);
|
|
160
|
+
at = close + 1;
|
|
161
|
+
if (line.startsWith(`"'"'`, at)) {
|
|
162
|
+
word += "'";
|
|
163
|
+
at += 3;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const end = line.indexOf(" ", at);
|
|
171
|
+
word = line.slice(at, end < 0 ? line.length : end);
|
|
172
|
+
if (!BARE_WORD.test(word))
|
|
173
|
+
return null;
|
|
174
|
+
at += word.length;
|
|
175
|
+
}
|
|
176
|
+
words.push(word);
|
|
177
|
+
if (at === line.length)
|
|
178
|
+
break;
|
|
179
|
+
if (line[at] !== " " || at + 1 === line.length)
|
|
180
|
+
return null;
|
|
181
|
+
at += 1;
|
|
182
|
+
}
|
|
183
|
+
return words.length > 0 && srtQuote(words) === line ? words : null;
|
|
184
|
+
}
|
|
185
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
186
|
+
/**
|
|
187
|
+
* `command` (the runtime's macOS invocation) with `rules` appended to its profile, made again from its words. The
|
|
188
|
+
* runtime's invocation is exactly `env [-u NAME]… [NAME=value]… /usr/bin/sandbox-exec -p <profile> <shell> -c
|
|
189
|
+
* <command>`, each word quoted its way, and `<command>` is `expected` when given. Any other line (an operator, a word
|
|
190
|
+
* before `env` or after the command, another program) is refused, so the launch doesn't run.
|
|
191
|
+
*/
|
|
192
|
+
export function withSeatbeltRules(command, rules, expected) {
|
|
193
|
+
if (rules.includes("'"))
|
|
194
|
+
throw new Error("a Seatbelt rule Toolroll adds can't hold a quote");
|
|
195
|
+
const shape = () => new Error("the sandbox runtime's command isn't the shape Toolroll knows");
|
|
196
|
+
const words = srtWords(command);
|
|
197
|
+
if (words === null || words[0] !== "env")
|
|
198
|
+
throw shape();
|
|
199
|
+
const exec = words.indexOf(SANDBOX_EXEC);
|
|
200
|
+
if (exec < 1 || words.length !== exec + 6 || words[exec + 1] !== "-p" || words[exec + 4] !== "-c")
|
|
201
|
+
throw shape();
|
|
202
|
+
for (let index = 1; index < exec; index += 1) {
|
|
203
|
+
if (words[index] === "-u") {
|
|
204
|
+
if (!ENV_NAME.test(words[index + 1] ?? ""))
|
|
205
|
+
throw shape();
|
|
206
|
+
index += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const name = words[index].split("=", 1)[0];
|
|
210
|
+
if (!words[index].includes("=") || !ENV_NAME.test(name))
|
|
211
|
+
throw shape();
|
|
212
|
+
}
|
|
213
|
+
const profile = words[exec + 2];
|
|
214
|
+
if (!profile.startsWith("(version 1)\n(deny default"))
|
|
215
|
+
throw shape();
|
|
216
|
+
if (!/^\/[A-Za-z0-9_./-]+$/.test(words[exec + 3]))
|
|
217
|
+
throw shape();
|
|
218
|
+
if (expected !== undefined && words[exec + 5] !== expected)
|
|
219
|
+
throw shape();
|
|
220
|
+
return srtQuote([...words.slice(0, exec + 2), `${profile}\n${rules}\n`, ...words.slice(exec + 3)]);
|
|
221
|
+
}
|
|
222
|
+
/** The runtime's macOS invocation for `file args` run from `cwd`: a command line for /bin/sh. */
|
|
223
|
+
export function srtMacosCommand(file, args, terms, cwd = process.cwd(), temp) {
|
|
224
|
+
const network = terms.network;
|
|
225
|
+
const params = {
|
|
226
|
+
command: shellQuote([file, ...args]),
|
|
227
|
+
needsNetworkRestriction: network.kind === "proxy",
|
|
228
|
+
...(network.kind === "proxy" ? {
|
|
229
|
+
httpProxyPort: network.httpPort,
|
|
230
|
+
...(network.socksPort === undefined ? {} : { socksProxyPort: network.socksPort }),
|
|
231
|
+
...(network.token === undefined ? {} : { proxyAuthToken: network.token }),
|
|
232
|
+
allowLocalBinding: network.localServices,
|
|
233
|
+
...(network.unixSockets.length === 0 ? {} : { allowUnixSockets: network.unixSockets }),
|
|
234
|
+
} : {}),
|
|
235
|
+
...(terms.allowMachLookup.length === 0 ? {} : { allowMachLookup: terms.allowMachLookup }),
|
|
236
|
+
readConfig: terms.readConfig,
|
|
237
|
+
writeConfig: terms.writeConfig,
|
|
238
|
+
};
|
|
239
|
+
// The runtime anchors its mandatory write denies at this process's working folder: the launch's, for this call.
|
|
240
|
+
const before = process.cwd();
|
|
241
|
+
process.chdir(cwd);
|
|
242
|
+
let command;
|
|
243
|
+
try {
|
|
244
|
+
command = withRuntimeTemp(temp, () => wrapCommandWithSandboxMacOS(params));
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
process.chdir(before);
|
|
248
|
+
}
|
|
249
|
+
return withSeatbeltRules(command, seatbeltRulesFor(terms), params.command);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* macOS: what every sandboxed launch runs first, outside the sandbox, before it execs the runtime's command: a session of
|
|
253
|
+
* its own (setsid, syscall 147), so no terminal of whoever started Toolroll is its controlling terminal to reach. A
|
|
254
|
+
* launch that is a session leader already (a live terminal's program, its own terminal made for it) keeps that one; any
|
|
255
|
+
* other that still has a controlling terminal (getsid, syscall 310) is refused. Perl with -t reads neither PERL5OPT nor
|
|
256
|
+
* PERL5LIB, and -X keeps its taint warnings off the launch's output.
|
|
257
|
+
*/
|
|
258
|
+
export const DETACH = ["-t", "-X", "-e",
|
|
259
|
+
'syscall(147) != -1 or syscall(310, 0) == $$ or !open(my $t, "<", "/dev/tty") or die "toolroll: the sandboxed launch kept a terminal\\n"; exec { $ARGV[0] } @ARGV; die "toolroll: the sandbox could not start: $!\\n";'];
|
|
260
|
+
/** The Linux helper (sandbox-launch.ts) and the environment variable its launch is handed in. */
|
|
261
|
+
export const SANDBOX_LAUNCH_SPEC = "TOOLROLL_SANDBOX_LAUNCH";
|
|
262
|
+
export const sandboxLaunchHelper = () => {
|
|
263
|
+
const beside = fileURLToPath(new URL("./sandbox-launch.js", import.meta.url));
|
|
264
|
+
// Run from the sources (the test suite), the built helper is the package's own.
|
|
265
|
+
return existsSync(beside) ? beside : fileURLToPath(new URL("../dist/sandbox-launch.js", import.meta.url));
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* `file args` inside the runtime's sandbox, as a spawn: on macOS `/bin/sh -c <its command>` in a session of its own
|
|
269
|
+
* (DETACH); on Linux the helper,
|
|
270
|
+
* handed the launch in its environment (never its arguments, which other users can list).
|
|
271
|
+
*/
|
|
272
|
+
export function srtSpawn(file, args, terms, cwd = process.cwd(), temp, platform = process.platform) {
|
|
273
|
+
if (platform === "darwin") {
|
|
274
|
+
const command = srtMacosCommand(file, args, terms, cwd, temp);
|
|
275
|
+
if (!command.includes(SANDBOX_EXEC))
|
|
276
|
+
throw new Error("the sandbox runtime returned the command unsandboxed");
|
|
277
|
+
return { file: "/usr/bin/perl", args: [...DETACH, "/bin/sh", "-c", command], env: {} };
|
|
278
|
+
}
|
|
279
|
+
const spec = { file, args: [...args], cwd, terms, ...(temp === undefined ? {} : { temp }) };
|
|
280
|
+
return { file: process.execPath, args: [sandboxLaunchHelper()], env: { [SANDBOX_LAUNCH_SPEC]: JSON.stringify(spec) } };
|
|
281
|
+
}
|
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
|
+
};
|