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.
Files changed (68) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +43 -17
  3. package/THIRD_PARTY_NOTICES.md +21 -1
  4. package/dist/agent-fence.d.ts +180 -115
  5. package/dist/agent-fence.js +413 -535
  6. package/dist/agentconfig.js +6 -1
  7. package/dist/browser/THIRD_PARTY_NOTICES.txt +21 -0
  8. package/dist/builder.js +24 -4
  9. package/dist/check-access.d.ts +6 -9
  10. package/dist/check-access.js +7 -10
  11. package/dist/coding-provider.d.ts +14 -0
  12. package/dist/coding-provider.js +33 -1
  13. package/dist/coding-workspace.d.ts +15 -0
  14. package/dist/coding-workspace.js +9 -2
  15. package/dist/contracts/console-api.d.ts +1 -0
  16. package/dist/contracts/console-api.js +1 -0
  17. package/dist/doctor.d.ts +7 -0
  18. package/dist/doctor.js +4 -0
  19. package/dist/egress.d.ts +2 -0
  20. package/dist/egress.js +13 -4
  21. package/dist/exec.d.ts +17 -3
  22. package/dist/exec.js +54 -17
  23. package/dist/flow-actions.d.ts +1 -0
  24. package/dist/flow-actions.js +3 -2
  25. package/dist/flow-code.d.ts +31 -2
  26. package/dist/flow-code.js +57 -5
  27. package/dist/flow-steps.d.ts +4 -0
  28. package/dist/flow-steps.js +9 -5
  29. package/dist/flow-triggers.d.ts +4 -0
  30. package/dist/flow-triggers.js +8 -3
  31. package/dist/invoke.d.ts +18 -8
  32. package/dist/invoke.js +98 -115
  33. package/dist/operate.js +46 -0
  34. package/dist/project-delete-ui.d.ts +7 -6
  35. package/dist/project-delete-ui.js +36 -2
  36. package/dist/project-sandbox.d.ts +59 -0
  37. package/dist/project-sandbox.js +83 -0
  38. package/dist/project-tools.d.ts +29 -5
  39. package/dist/project-tools.js +43 -7
  40. package/dist/release.d.ts +1 -1
  41. package/dist/release.js +1 -0
  42. package/dist/sandbox-launch.d.ts +1 -0
  43. package/dist/sandbox-launch.js +148 -0
  44. package/dist/sandbox-runtime.d.ts +62 -0
  45. package/dist/sandbox-runtime.js +140 -0
  46. package/dist/scope.d.ts +28 -0
  47. package/dist/scope.js +25 -5
  48. package/dist/scout-browser-lock.d.ts +69 -0
  49. package/dist/scout-browser-lock.js +69 -0
  50. package/dist/scout-browser.d.ts +60 -1
  51. package/dist/scout-browser.js +200 -4
  52. package/dist/scout.d.ts +9 -4
  53. package/dist/scout.js +14 -4
  54. package/dist/serve.js +3 -1
  55. package/dist/server/render-tasks.js +4 -1
  56. package/dist/server/route-table.js +1 -0
  57. package/dist/server/settings.js +27 -0
  58. package/dist/store.d.ts +31 -1
  59. package/dist/store.js +83 -4
  60. package/dist/subagent-tools.d.ts +1 -0
  61. package/dist/subagent-tools.js +7 -5
  62. package/dist/tool-broker.d.ts +2 -1
  63. package/dist/tool-broker.js +2 -2
  64. package/dist/tool-launcher.js +10 -16
  65. package/package.json +2 -1
  66. package/scripts/provider-canary.mjs +5 -6
  67. package/dist/egress-bridge.d.ts +0 -17
  68. 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
- const withNetwork = { ...(network === undefined ? {} : { network }), ...(writes === undefined ? {} : { writes }), ...(checkAccess === undefined ? {} : { checkAccess }) };
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
+ };
@@ -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 that starts nothing. */
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>;