toolroll 0.9.68 → 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 +81 -0
- package/README.md +43 -17
- package/THIRD_PARTY_NOTICES.md +21 -1
- package/dist/agent-fence.d.ts +204 -81
- package/dist/agent-fence.js +467 -397
- package/dist/agentconfig.d.ts +1 -1
- package/dist/agentconfig.js +11 -2
- package/dist/batch-checks.js +14 -26
- package/dist/browser/THIRD_PARTY_NOTICES.txt +21 -0
- package/dist/builder.d.ts +9 -0
- package/dist/builder.js +50 -4
- package/dist/check-access.d.ts +51 -0
- package/dist/check-access.js +77 -0
- 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 +2 -0
- package/dist/contracts/console-api.js +2 -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 +50 -3
- package/dist/invoke.js +204 -66
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +119 -0
- package/dist/project-delete-ui.d.ts +23 -6
- package/dist/project-delete-ui.js +94 -1
- 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/result-follow-ups.js +10 -15
- 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 +37 -0
- package/dist/scope.js +32 -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 +12 -1
- package/dist/server/http.d.ts +2 -0
- package/dist/server/render-results.d.ts +3 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.js +5 -1
- package/dist/server/route-table.js +2 -0
- package/dist/server/settings.js +64 -0
- package/dist/server/tasks.js +2 -0
- package/dist/store.d.ts +59 -2
- package/dist/store.js +166 -4
- package/dist/subagent-tools.d.ts +1 -0
- package/dist/subagent-tools.js +7 -5
- package/dist/surface.js +2 -1
- 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
package/dist/scope.js
CHANGED
|
@@ -41,6 +41,8 @@ export { isAcpProfile };
|
|
|
41
41
|
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
42
42
|
import { networkFromValue, networkWords } from "./egress.js";
|
|
43
43
|
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
44
|
+
import { checkAccessWords, isCheckAccess } from "./check-access.js";
|
|
45
|
+
import { MAX_MACH_LOOKUP, MAX_READ_DENY, sandboxTermsWords, validMachLookup, validReadDeny } from "./project-sandbox.js";
|
|
44
46
|
import { z } from "zod";
|
|
45
47
|
import {} from "./contracts/acceptance-terms.js";
|
|
46
48
|
import { parseContract } from "./contracts/contract.js";
|
|
@@ -354,9 +356,31 @@ export function profileFromJson(json) {
|
|
|
354
356
|
const writes = p["writes"];
|
|
355
357
|
if (writes !== undefined && !isWritesMode(writes))
|
|
356
358
|
return null;
|
|
357
|
-
|
|
359
|
+
// Check access is sealed only when wider than sandboxed: an explicit "sandboxed", or any other word, is not this code's.
|
|
360
|
+
const sealedAccess = p["checkAccess"];
|
|
361
|
+
if (sealedAccess !== undefined && (sealedAccess === "sandboxed" || !isCheckAccess(sealedAccess)))
|
|
362
|
+
return null;
|
|
363
|
+
const checkAccess = sealedAccess;
|
|
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 }) };
|
|
358
382
|
if (p["provider"] === "claude") {
|
|
359
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes"]) &&
|
|
383
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
360
384
|
str(p["model"]) &&
|
|
361
385
|
(p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
|
|
362
386
|
num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
|
|
@@ -378,7 +402,7 @@ export function profileFromJson(json) {
|
|
|
378
402
|
return null;
|
|
379
403
|
}
|
|
380
404
|
if (p["provider"] === "codex" || p["provider"] === "openrouter") {
|
|
381
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes"]) &&
|
|
405
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
382
406
|
str(p["model"]) &&
|
|
383
407
|
(p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
|
|
384
408
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -400,7 +424,7 @@ export function profileFromJson(json) {
|
|
|
400
424
|
return null;
|
|
401
425
|
}
|
|
402
426
|
if (p["provider"] === "gemini") {
|
|
403
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes"]) &&
|
|
427
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
404
428
|
str(p["model"]) &&
|
|
405
429
|
(p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
|
|
406
430
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -422,7 +446,7 @@ export function profileFromJson(json) {
|
|
|
422
446
|
return null;
|
|
423
447
|
}
|
|
424
448
|
if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
|
|
425
|
-
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes"]) &&
|
|
449
|
+
if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes", "checkAccess", "fencePolicy", "readDeny", "machLookup"]) &&
|
|
426
450
|
str(p["model"]) &&
|
|
427
451
|
p["permission"] === "full" &&
|
|
428
452
|
p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
|
|
@@ -1271,6 +1295,9 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1271
1295
|
// What the build may reach (egress.ts), part of what the yes agrees to.
|
|
1272
1296
|
...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
|
|
1273
1297
|
...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
|
|
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))),
|
|
1274
1301
|
` reference ${scope.digest}`,
|
|
1275
1302
|
` approved ${describeApproval(approval)}`,
|
|
1276
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>;
|
package/dist/scout-browser.js
CHANGED
|
@@ -10,21 +10,32 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Playwright MCP 0.0.82 has no per-client token for its HTTP transport; it answers only on 127.0.0.1, only for
|
|
12
12
|
* that host name (its own host check), and only while this run's scout does.
|
|
13
|
+
*
|
|
14
|
+
* An explicit exception (docs/plans/sandbox-parity.md C4, owner Q-A): Toolroll can't fence this process without
|
|
15
|
+
* switching Chrome's sandbox off, so its launch is held to a contract instead, and refused when any part fails
|
|
16
|
+
* (`browserContractProblem`): the pinned Playwright MCP and its Chrome for Testing are installed in Toolroll's own
|
|
17
|
+
* helpers folder (inside the state folder every build's fence denies) and run by Toolroll's own Node; its arguments are
|
|
18
|
+
* exactly the expected ones (Chrome's sandbox on, the scout's proxy on, its image folder as its output and working
|
|
19
|
+
* folder); its environment is an allowlist. The run records which exception it ran under.
|
|
13
20
|
*/
|
|
14
21
|
import { spawn } from "node:child_process";
|
|
15
22
|
import { request as httpRequest } from "node:http";
|
|
16
23
|
import { connect, createServer } from "node:net";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
27
|
+
import { run } from "./exec.js";
|
|
28
|
+
import { SCOUT_BROWSER_LOCK, SCOUT_BROWSER_MANIFEST } from "./scout-browser-lock.js";
|
|
19
29
|
import { sleep } from "./sleep.js";
|
|
20
30
|
/** The public page the check opens through the scout's proxy. */
|
|
21
31
|
export const PREFLIGHT_PAGE = "https://example.com/";
|
|
22
32
|
const PREFLIGHT_TIMEOUT_MS = 60_000;
|
|
23
33
|
const CLOSE_GRACE_MS = 5_000;
|
|
24
34
|
const IMAGE = /\.(png|jpe?g)$/i;
|
|
25
|
-
/** How a scout gets its browser: started for real, or, for a test's stub agent, a placeholder
|
|
35
|
+
/** How a scout gets its browser: started for real under its launch contract, or, for a test's stub agent, a placeholder
|
|
36
|
+
* that starts nothing. */
|
|
26
37
|
export function starterFor(agentInjected) {
|
|
27
|
-
return agentInjected ? unstarted :
|
|
38
|
+
return agentInjected ? unstarted : startContractedBrowser;
|
|
28
39
|
}
|
|
29
40
|
const unstarted = async () => ({ ok: true, server: { url: "http://127.0.0.1:9/mcp", close: async () => undefined } });
|
|
30
41
|
/** Start the browser tool outside the fence and check it opens a public page and saves a screenshot. */
|
|
@@ -227,3 +238,188 @@ function oneLine(text) {
|
|
|
227
238
|
// eslint-disable-next-line no-control-regex
|
|
228
239
|
return text.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim().slice(0, 200);
|
|
229
240
|
}
|
|
241
|
+
// ---- the launch contract (docs/plans/sandbox-parity.md C4) ----
|
|
242
|
+
/** The pinned browser tool, installed once into Toolroll's helpers folder. */
|
|
243
|
+
export const SCOUT_BROWSER_PACKAGE = "@playwright/mcp@0.0.82";
|
|
244
|
+
const HELPER_NAME = "scout-browser-0.0.82";
|
|
245
|
+
/** The names of the worker's environment the browser tool gets: an allowlist, never a denylist. */
|
|
246
|
+
export const SCOUT_BROWSER_ENV = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR",
|
|
247
|
+
"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"];
|
|
248
|
+
/** What the run records of the exception it ran under. */
|
|
249
|
+
export const SCOUT_BROWSER_CONTAINMENT = "Chrome's sandbox";
|
|
250
|
+
/** Where the pinned browser lives under `helpers`. */
|
|
251
|
+
export function scoutBrowserHelper(helpers) {
|
|
252
|
+
const folder = join(helpers, HELPER_NAME);
|
|
253
|
+
return { folder, cli: join(folder, "node_modules", "@playwright", "mcp", "cli.js"), browsers: join(folder, "browsers") };
|
|
254
|
+
}
|
|
255
|
+
/** The scout's browser tool: the pinned helper, run by Toolroll's own Node, headless, on its bundled Chrome. */
|
|
256
|
+
export function scoutBrowserTool(helpers) {
|
|
257
|
+
return { command: process.execPath, args: [scoutBrowserHelper(helpers).cli, "--headless", "--browser", "chromium"] };
|
|
258
|
+
}
|
|
259
|
+
/** A folder of Toolroll's own: a real folder (no link at it), this user's, private (0700). */
|
|
260
|
+
function privateFolder(path) {
|
|
261
|
+
try {
|
|
262
|
+
const held = lstatSync(path);
|
|
263
|
+
return held.isDirectory() && !held.isSymbolicLink() && (process.getuid === undefined || held.uid === process.getuid()) && (held.mode & 0o077) === 0;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const within = (child, parent) => { const rel = relative(parent, child); return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel); };
|
|
270
|
+
/**
|
|
271
|
+
* Why a browser launch breaks its contract, or null when it holds. Every input it trusts is the plane's: the helpers
|
|
272
|
+
* folder and the proxy come from `options.contract`, never from the launch. The executable must be Toolroll's own
|
|
273
|
+
* Node; the script the pinned helper's, inside a private helpers folder; the arguments exactly the expected ones (no
|
|
274
|
+
* way to switch Chrome's sandbox off, point at another browser or executable, or drop the proxy); the working folder
|
|
275
|
+
* the image folder; the environment only allowlisted names.
|
|
276
|
+
*/
|
|
277
|
+
export function browserContractProblem(launch, options) {
|
|
278
|
+
const contract = options.contract;
|
|
279
|
+
if (contract === undefined)
|
|
280
|
+
return "the browser has no launch contract";
|
|
281
|
+
const real = (path) => { try {
|
|
282
|
+
return realpathSync(path);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
} };
|
|
287
|
+
if (real(launch.command) === null || real(launch.command) !== real(process.execPath))
|
|
288
|
+
return "the browser isn't run by Toolroll's own Node";
|
|
289
|
+
if (!privateFolder(contract.helpers))
|
|
290
|
+
return "Toolroll's helpers folder isn't a private folder of its own";
|
|
291
|
+
const helper = scoutBrowserHelper(contract.helpers);
|
|
292
|
+
const script = launch.args[0] === undefined ? null : real(launch.args[0]);
|
|
293
|
+
const helpers = real(contract.helpers);
|
|
294
|
+
if (script === null || helpers === null || !within(script, helpers) || script !== real(helper.cli))
|
|
295
|
+
return "the browser tool isn't the pinned one in Toolroll's helpers folder";
|
|
296
|
+
const expected = [launch.args[0], "--headless", "--browser", "chromium", "--isolated", "--output-dir", options.folder, "--proxy-server", contract.proxy, "--proxy-bypass", "<-loopback>"];
|
|
297
|
+
if (launch.args.length !== expected.length || launch.args.some((one, index) => one !== expected[index]))
|
|
298
|
+
return "the browser's arguments aren't the contract's (Chrome's sandbox and the scout's proxy stay on)";
|
|
299
|
+
const extra = Object.keys(options.env).filter(name => options.env[name] !== undefined && !SCOUT_BROWSER_ENV.includes(name));
|
|
300
|
+
if (extra.length > 0)
|
|
301
|
+
return `the browser's environment carries names outside its allowlist (${extra.slice(0, 5).join(", ")})`;
|
|
302
|
+
if (options.env["HTTPS_PROXY"] !== contract.proxy || options.env["HTTP_PROXY"] !== contract.proxy)
|
|
303
|
+
return "the browser's environment doesn't point at the scout's proxy";
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
/** The registry the helper is installed from: pinned, whatever any npm configuration says. */
|
|
307
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
308
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
309
|
+
/** The pinned lockfile's digest, recorded when the helper is installed and checked before every use. */
|
|
310
|
+
export const SCOUT_BROWSER_LOCK_DIGEST = sha256(JSON.stringify(SCOUT_BROWSER_LOCK));
|
|
311
|
+
/** The npm of the Node running Toolroll (`lib/node_modules/npm/bin/npm-cli.js` beside it), run by that Node by path:
|
|
312
|
+
* never an `npm` found on PATH. Null where this Node has none beside it (a bundled runtime). */
|
|
313
|
+
export function ownNpm(execPath = process.execPath) {
|
|
314
|
+
const cli = join(dirname(dirname(execPath)), "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
315
|
+
try {
|
|
316
|
+
return realpathSync(cli);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Install the pinned browser into the helpers folder once. The worker runs this outside every fence, so nothing a build
|
|
324
|
+
* could have written may steer it: npm is Toolroll's own Node's, run by path; HOME is the staging folder and the user
|
|
325
|
+
* and global npm configurations are an empty file there (no ~/.npmrc, no global npmrc, no registry, script or Node
|
|
326
|
+
* option from either); the registry is pinned; install scripts never run; `npm ci` installs exactly the pinned lockfile
|
|
327
|
+
* (scout-browser-lock.ts) and refuses a tarball whose integrity differs; the installed versions are checked against it
|
|
328
|
+
* before ready.json is written. The npm cache and Chrome for Testing stay in the folder, never in the user's caches.
|
|
329
|
+
* Residual: Chrome for Testing comes from Playwright's own download over HTTPS, without a pinned hash.
|
|
330
|
+
*/
|
|
331
|
+
export async function ensureScoutBrowserHelper(helpers, timeoutMs = 600_000) {
|
|
332
|
+
const helper = scoutBrowserHelper(helpers);
|
|
333
|
+
if (helperReady(helper.folder))
|
|
334
|
+
return { ok: true };
|
|
335
|
+
const npm = ownNpm();
|
|
336
|
+
if (npm === null)
|
|
337
|
+
return { ok: false, reason: "Toolroll's own Node has no npm beside it, so the pinned browser can't be installed" };
|
|
338
|
+
try {
|
|
339
|
+
mkdirSync(helpers, { recursive: true, mode: 0o700 });
|
|
340
|
+
}
|
|
341
|
+
catch { /* checked next */ }
|
|
342
|
+
if (!privateFolder(helpers))
|
|
343
|
+
return { ok: false, reason: "Toolroll's helpers folder isn't a private folder of its own" };
|
|
344
|
+
const staging = mkdtempSync(join(helpers, ".install-"));
|
|
345
|
+
try {
|
|
346
|
+
writeFileSync(join(staging, "package.json"), JSON.stringify(SCOUT_BROWSER_MANIFEST), { mode: 0o600 });
|
|
347
|
+
writeFileSync(join(staging, "package-lock.json"), JSON.stringify(SCOUT_BROWSER_LOCK), { mode: 0o600 });
|
|
348
|
+
const { env, args } = helperNpm(staging, npm);
|
|
349
|
+
const installed = await run(process.execPath, [...args, "ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: staging, env, envAllowlist: [], timeoutMs });
|
|
350
|
+
if (installed.code !== 0)
|
|
351
|
+
return { ok: false, reason: `the pinned browser couldn't be installed (npm exit ${installed.code})` };
|
|
352
|
+
const drift = lockDrift(staging);
|
|
353
|
+
if (drift !== null)
|
|
354
|
+
return { ok: false, reason: `the installed browser isn't the pinned one (${drift})` };
|
|
355
|
+
const browsers = await run(process.execPath, [join(staging, "node_modules", "playwright", "cli.js"), "install", "chromium"], { cwd: staging, env, envAllowlist: [], timeoutMs });
|
|
356
|
+
if (browsers.code !== 0)
|
|
357
|
+
return { ok: false, reason: `the pinned browser's Chrome couldn't be installed (exit ${browsers.code})` };
|
|
358
|
+
rmSync(join(staging, "npm-cache"), { recursive: true, force: true });
|
|
359
|
+
writeFileSync(join(staging, "ready.json"), JSON.stringify({ package: SCOUT_BROWSER_PACKAGE, lock: SCOUT_BROWSER_LOCK_DIGEST }), { mode: 0o600 });
|
|
360
|
+
try {
|
|
361
|
+
renameSync(staging, helper.folder);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
if (!helperReady(helper.folder))
|
|
365
|
+
return { ok: false, reason: "the pinned browser couldn't be put in place" };
|
|
366
|
+
}
|
|
367
|
+
return { ok: true };
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
rmSync(staging, { recursive: true, force: true });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** How the helper's npm runs in `staging`: Toolroll's own npm by path, with its home, user and global configuration
|
|
374
|
+
* the staging folder's own empty ones, the registry and cache pinned; the environment only these names. */
|
|
375
|
+
export function helperNpm(staging, npm) {
|
|
376
|
+
// Two empty files: npm refuses to load one file as both.
|
|
377
|
+
const user = join(staging, "user.npmrc"), global = join(staging, "global.npmrc");
|
|
378
|
+
for (const file of [user, global])
|
|
379
|
+
writeFileSync(file, "", { mode: 0o600 });
|
|
380
|
+
const env = {
|
|
381
|
+
PATH: [dirname(process.execPath), "/usr/bin", "/bin"].join(":"), HOME: staging, PLAYWRIGHT_BROWSERS_PATH: join(staging, "browsers"),
|
|
382
|
+
NPM_CONFIG_USERCONFIG: user, NPM_CONFIG_GLOBALCONFIG: global, npm_config_userconfig: user, npm_config_globalconfig: global,
|
|
383
|
+
};
|
|
384
|
+
return { env, args: [npm, "--no-update-notifier", `--registry=${NPM_REGISTRY}`, "--userconfig", user, "--globalconfig", global, "--cache", join(staging, "npm-cache")] };
|
|
385
|
+
}
|
|
386
|
+
/** Whether a helper folder holds the pinned install: its ready note names this lockfile, and its tree still matches. */
|
|
387
|
+
function helperReady(folder) {
|
|
388
|
+
try {
|
|
389
|
+
const ready = JSON.parse(readFileSync(join(folder, "ready.json"), "utf8"));
|
|
390
|
+
return ready.lock === SCOUT_BROWSER_LOCK_DIGEST && lockDrift(folder) === null;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** What of an installed tree differs from the pinned lockfile (a package missing or at another version), or null. */
|
|
397
|
+
export function lockDrift(folder) {
|
|
398
|
+
for (const [path, entry] of Object.entries(SCOUT_BROWSER_LOCK.packages)) {
|
|
399
|
+
if (path === "" || entry.version === undefined)
|
|
400
|
+
continue;
|
|
401
|
+
let version;
|
|
402
|
+
try {
|
|
403
|
+
version = JSON.parse(readFileSync(join(folder, path, "package.json"), "utf8")).version;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
version = undefined;
|
|
407
|
+
}
|
|
408
|
+
if (version !== entry.version)
|
|
409
|
+
return `${path.replace(/^node_modules\//, "")} is ${typeof version === "string" ? version : "missing"}, pinned ${entry.version}`;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
/** The production starter: the pinned helper in place, then the launch held to its contract, then started. */
|
|
414
|
+
export async function startContractedBrowser(launch, options) {
|
|
415
|
+
if (options.contract === undefined)
|
|
416
|
+
return { ok: false, reason: "the browser has no launch contract" };
|
|
417
|
+
const installed = await ensureScoutBrowserHelper(options.contract.helpers);
|
|
418
|
+
if (!installed.ok)
|
|
419
|
+
return installed;
|
|
420
|
+
const env = { ...options.env, PLAYWRIGHT_BROWSERS_PATH: scoutBrowserHelper(options.contract.helpers).browsers };
|
|
421
|
+
const problem = browserContractProblem(launch, { ...options, env: options.env });
|
|
422
|
+
if (problem !== null)
|
|
423
|
+
return { ok: false, reason: problem };
|
|
424
|
+
return startScoutBrowser(launch, { ...options, env });
|
|
425
|
+
}
|
package/dist/scout.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Store } from "./store.js";
|
|
|
3
3
|
import { type ParsedDecision, type Problem } from "./decision.js";
|
|
4
4
|
import { type ParsedReport, type ReportProblem } from "./scout-report.js";
|
|
5
5
|
import type { Runner } from "./builder.js";
|
|
6
|
-
import { type ToolSpec } from "./project-tools.js";
|
|
7
6
|
import * as browserCheck from "./scout-browser.js";
|
|
8
7
|
/** The scout's own headless browser: the pinned Playwright tool, saving its
|
|
9
8
|
* screenshots straight into the run's image folder. */
|
|
@@ -16,7 +15,10 @@ export declare const SCOUT_BROWSER = "toolroll-browser";
|
|
|
16
15
|
* clean-tree proof.
|
|
17
16
|
*/
|
|
18
17
|
export declare const SCOUT_ALLOWED_TOOLS: readonly string[];
|
|
19
|
-
export declare function scoutBrowserLaunch(tool:
|
|
18
|
+
export declare function scoutBrowserLaunch(tool: {
|
|
19
|
+
command: string | null;
|
|
20
|
+
args: readonly string[];
|
|
21
|
+
} | null, imageFolder: string, proxy: string): browserCheck.BrowserLaunch | null;
|
|
20
22
|
/** What the fenced scout gets: the running browser's loopback address, nothing it could launch itself. */
|
|
21
23
|
export declare function scoutBrowser(url: string): Record<string, unknown>;
|
|
22
24
|
/** The scout's environment while its proxy runs (review 827): web fetch and the provider's own traffic go through
|
|
@@ -62,8 +64,11 @@ export type ScoutRequest = {
|
|
|
62
64
|
outputDir?: string;
|
|
63
65
|
/** The project's own demo or dev server, the one non-public address the scout's browser may open. */
|
|
64
66
|
demoUrl?: string | null;
|
|
65
|
-
/** The browser to launch; left out, the
|
|
66
|
-
browserTool?:
|
|
67
|
+
/** The browser to launch; left out, the pinned helper in Toolroll's helpers folder. Null runs the scout without one. */
|
|
68
|
+
browserTool?: {
|
|
69
|
+
command: string | null;
|
|
70
|
+
args: readonly string[];
|
|
71
|
+
} | null;
|
|
67
72
|
};
|
|
68
73
|
export type ReportArtifact = {
|
|
69
74
|
key: string;
|
package/dist/scout.js
CHANGED
|
@@ -16,7 +16,7 @@ import { skillsContext } from "./project-skills.js";
|
|
|
16
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
17
17
|
import { mkdtempSync, realpathSync, rmSync, unlinkSync } from "node:fs";
|
|
18
18
|
import { homedir, tmpdir } from "node:os";
|
|
19
|
-
import { join, relative, isAbsolute } from "node:path";
|
|
19
|
+
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
20
20
|
import { run } from "./exec.js";
|
|
21
21
|
import { auditOf } from "./provider.js";
|
|
22
22
|
import { currentClaim, heartbeat } from "./claim.js";
|
|
@@ -32,7 +32,7 @@ import { openLiveLog } from "./live.js";
|
|
|
32
32
|
import { proveTreeUntouched, snapshotIgnored } from "./tree-proof.js";
|
|
33
33
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
34
34
|
import { CLAUDE_LIMITS } from "./scope.js";
|
|
35
|
-
import {
|
|
35
|
+
import { projectToolsOf, toolLaunchFor } from "./project-tools.js";
|
|
36
36
|
import { grantOf, researchToolsOf } from "./mcp-connect.js";
|
|
37
37
|
import { startScoutProxy } from "./scout-net.js";
|
|
38
38
|
import * as browserCheck from "./scout-browser.js";
|
|
@@ -312,7 +312,11 @@ async function scoutWith(store, request, outputDir) {
|
|
|
312
312
|
// Every scout runs behind its public-web-only proxy (reviews 826, 827): its browser, when this install has one,
|
|
313
313
|
// and its web fetch both go through it. A proxy that cannot start leaves the scout without the web, reading only
|
|
314
314
|
// the checkout, never with an unfiltered one.
|
|
315
|
-
|
|
315
|
+
// The pinned browser in Toolroll's helpers folder, run under its launch contract (scout-browser.ts); none where the
|
|
316
|
+
// plane has no state folder.
|
|
317
|
+
const database = store.databaseFile();
|
|
318
|
+
const helpers = database === null || database === "" || database === ":memory:" ? null : join(dirname(realpathSync(database)), "helpers");
|
|
319
|
+
const browserTool = request.browserTool === undefined ? (helpers === null ? null : browserCheck.scoutBrowserTool(helpers)) : request.browserTool;
|
|
316
320
|
const demoUrl = request.demoUrl ?? null;
|
|
317
321
|
let proxy = null;
|
|
318
322
|
let browserServer = null;
|
|
@@ -324,9 +328,11 @@ async function scoutWith(store, request, outputDir) {
|
|
|
324
328
|
// The preflight (run 2356, review 828): the worker starts the browser outside the fence and opens a public page
|
|
325
329
|
// through the proxy before research begins. One that can't start is said once, in the brief and the report, and
|
|
326
330
|
// the scout researches without it.
|
|
331
|
+
// Its environment is an allowlist of the worker's, with the scout's proxy (the launch contract).
|
|
327
332
|
const started = launch === null || proxy === null ? null : await browserCheck.starterFor(agent !== undefined)(launch, {
|
|
328
|
-
env: { ...
|
|
333
|
+
env: { ...Object.fromEntries(browserCheck.SCOUT_BROWSER_ENV.filter(name => !(name in scoutProxyEnv(proxy.url)) && process.env[name] !== undefined).map(name => [name, process.env[name]])), ...scoutProxyEnv(proxy.url) },
|
|
329
334
|
folder: imageFolder,
|
|
335
|
+
...(helpers === null ? {} : { contract: { helpers, proxy: proxy.url } }),
|
|
330
336
|
}).catch((error) => ({ ok: false, reason: error instanceof Error ? error.message : String(error) }));
|
|
331
337
|
if (started !== null && started.ok)
|
|
332
338
|
browserServer = started.server;
|
|
@@ -367,6 +373,10 @@ async function scoutWith(store, request, outputDir) {
|
|
|
367
373
|
clock,
|
|
368
374
|
onStreamEvent: observe,
|
|
369
375
|
});
|
|
376
|
+
// The run says which exception its browser ran under: Chrome's own sandbox, not Toolroll's fence (recorded after the
|
|
377
|
+
// gateway, whose tool record would replace it).
|
|
378
|
+
if (browserServer !== null)
|
|
379
|
+
store.recordRunBrowser(request.runId, { containment: browserCheck.SCOUT_BROWSER_CONTAINMENT, tool: browserCheck.SCOUT_BROWSER_PACKAGE }, new Date());
|
|
370
380
|
}
|
|
371
381
|
finally {
|
|
372
382
|
if (pulseTimer !== undefined)
|