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/egress.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export declare function runNetworkFromValue(value: unknown): RunNetworkRecord |
|
|
|
96
96
|
export type EgressProxy = {
|
|
97
97
|
/** The proxy's address with this run's credentials, for HTTPS_PROXY and the like. */
|
|
98
98
|
url: string;
|
|
99
|
+
/** This run's credential alone: a client presenting it (with any user name: the sandbox runtime names its own) is served. */
|
|
100
|
+
secret: string;
|
|
99
101
|
port: number;
|
|
100
102
|
counts: () => EgressCounts;
|
|
101
103
|
close: () => Promise<void>;
|
package/dist/egress.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* and the project's added hosts), and that snapshot is what the approval binds and the launch enforces. Nothing at
|
|
7
7
|
* launch re-derives it from settings changed since or from files the agent wrote.
|
|
8
8
|
*
|
|
9
|
-
* The launch enforces it at the operating system (
|
|
10
|
-
* namespace on Linux) and reaches the world through one proxy of its own (startEgressProxy), which admits only the
|
|
9
|
+
* The launch enforces it at the operating system (Anthropic's sandbox-runtime: its Seatbelt network rules on macOS, a
|
|
10
|
+
* network namespace on Linux with the proxy relayed in) and reaches the world through one proxy of its own (startEgressProxy), which admits only the
|
|
11
11
|
* snapshot's hosts. The proxy never decrypts TLS: it restricts where a build connects, not what it sends there.
|
|
12
12
|
*/
|
|
13
13
|
import { createServer, request as httpRequest } from "node:http";
|
|
@@ -272,7 +272,7 @@ export async function startEgressProxy(options) {
|
|
|
272
272
|
const upstreamPort = options.upstreamPort ?? ((port) => port);
|
|
273
273
|
const lookup = options.lookup ?? (async (host) => (await dnsLookup(host, { all: true, verbatim: true })).map(one => one.address));
|
|
274
274
|
const secret = randomBytes(24).toString("base64url");
|
|
275
|
-
const expected = Buffer.from(
|
|
275
|
+
const expected = Buffer.from(secret);
|
|
276
276
|
const sockets = new Set();
|
|
277
277
|
const byHost = new Map();
|
|
278
278
|
let connections = 0, refused = 0, otherHosts = 0;
|
|
@@ -300,8 +300,16 @@ export async function startEgressProxy(options) {
|
|
|
300
300
|
}
|
|
301
301
|
catch { /* a listener's trouble is not the build's */ }
|
|
302
302
|
};
|
|
303
|
+
/** Basic credentials whose password is this run's secret, whatever the user name (the sandbox runtime puts its own
|
|
304
|
+
* attribution there). */
|
|
303
305
|
const authorized = (incoming) => {
|
|
304
|
-
const
|
|
306
|
+
const header = String(incoming.headers["proxy-authorization"] ?? "");
|
|
307
|
+
const basic = /^Basic ([A-Za-z0-9+/=]+)$/.exec(header)?.[1];
|
|
308
|
+
if (basic === undefined)
|
|
309
|
+
return false;
|
|
310
|
+
const decoded = Buffer.from(basic, "base64").toString("utf8");
|
|
311
|
+
const colon = decoded.indexOf(":");
|
|
312
|
+
const given = Buffer.from(colon < 0 ? "" : decoded.slice(colon + 1));
|
|
305
313
|
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
306
314
|
};
|
|
307
315
|
/** The addresses a request may reach, or why it may not. */
|
|
@@ -412,6 +420,7 @@ export async function startEgressProxy(options) {
|
|
|
412
420
|
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
413
421
|
return {
|
|
414
422
|
url: `http://run:${secret}@127.0.0.1:${port}`,
|
|
423
|
+
secret,
|
|
415
424
|
port,
|
|
416
425
|
counts: () => ({ connections, refused, hosts: [...byHost].map(([host, one]) => ({ host, ...one })).sort((a, b) => b.connections + b.refused - (a.connections + a.refused) || a.host.localeCompare(b.host)), otherHosts }),
|
|
417
426
|
close: () => new Promise(done => {
|
package/dist/exec.d.ts
CHANGED
|
@@ -183,18 +183,24 @@ export declare class ContainmentRefusal extends Error {
|
|
|
183
183
|
readonly name = "ContainmentRefusal";
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
|
-
* The command as it runs inside the agent fence (sandbox-
|
|
187
|
-
*
|
|
188
|
-
*
|
|
186
|
+
* The command as it runs inside the agent fence (Anthropic's sandbox-runtime wrapping it: agent-fence.ts
|
|
187
|
+
* sandboxedSpawn), with the environment that needs beside the caller's, or as it is when it has no fence. A spawn whose
|
|
188
|
+
* confinement is required and that no sandbox here can wrap is refused (ContainmentRefusal): nothing runs, and nothing
|
|
189
|
+
* falls back to running it unwrapped.
|
|
189
190
|
*/
|
|
190
191
|
export declare function confined(file: string, args: readonly string[], bag: {
|
|
191
192
|
fence?: readonly string[] | undefined;
|
|
192
193
|
fenceGrants?: FenceGrants | undefined;
|
|
193
194
|
confinement?: "required" | undefined;
|
|
195
|
+
cwd?: string | undefined;
|
|
196
|
+
temp?: string | undefined;
|
|
194
197
|
}): {
|
|
195
198
|
file: string;
|
|
196
199
|
args: string[];
|
|
200
|
+
env: Record<string, string>;
|
|
197
201
|
};
|
|
202
|
+
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
203
|
+
export declare function withSandboxEnv<T extends Record<string, string | undefined>>(env: T | undefined, added: Record<string, string>): T | Record<string, string | undefined> | undefined;
|
|
198
204
|
/** Run IDs are local to a database. Connections to the same file share
|
|
199
205
|
* custody; separate files and separate in-memory databases never do. */
|
|
200
206
|
export declare function runOwnerTag(store: Store, runId: number): string;
|
|
@@ -298,6 +304,14 @@ export type AcpControl = {
|
|
|
298
304
|
group: number;
|
|
299
305
|
}[] | null;
|
|
300
306
|
};
|
|
307
|
+
/**
|
|
308
|
+
* Whether a listed process is the agent's launch (`names`): by its listed name, or on Linux by the program it runs,
|
|
309
|
+
* since a listing there shows the main thread's name, which Node 24 sets to "MainThread".
|
|
310
|
+
*/
|
|
311
|
+
export declare function agentNamed(row: {
|
|
312
|
+
pid: number;
|
|
313
|
+
name: string;
|
|
314
|
+
}, names: readonly string[], programOf?: (pid: number) => string | null): boolean;
|
|
301
315
|
/** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
|
|
302
316
|
export declare const ACP_SHUTDOWN_MS: {
|
|
303
317
|
readonly close: 2000;
|
package/dist/exec.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* walks repos it has never seen, and one broken repo must not end the scan.
|
|
8
8
|
*/
|
|
9
9
|
import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
|
|
10
|
-
import { linuxFenceAvailable,
|
|
10
|
+
import { linuxFenceAvailable, macosFenceAvailable, sandboxedSpawn } from "./agent-fence.js";
|
|
11
11
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
12
|
-
import { realpathSync } from "node:fs";
|
|
12
|
+
import { readlinkSync, realpathSync } from "node:fs";
|
|
13
13
|
import { observeProcessTree, stopProcessTree } from "./process-tree.js";
|
|
14
14
|
import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
15
15
|
import { createContainer, currentContainment } from "./containment.js";
|
|
@@ -48,22 +48,35 @@ export class ContainmentRefusal extends Error {
|
|
|
48
48
|
name = "ContainmentRefusal";
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* The command as it runs inside the agent fence (sandbox-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* The command as it runs inside the agent fence (Anthropic's sandbox-runtime wrapping it: agent-fence.ts
|
|
52
|
+
* sandboxedSpawn), with the environment that needs beside the caller's, or as it is when it has no fence. A spawn whose
|
|
53
|
+
* confinement is required and that no sandbox here can wrap is refused (ContainmentRefusal): nothing runs, and nothing
|
|
54
|
+
* falls back to running it unwrapped.
|
|
54
55
|
*/
|
|
55
56
|
export function confined(file, args, bag) {
|
|
56
57
|
const fence = bag.fence ?? [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (fence.length > 0 &&
|
|
60
|
-
|
|
58
|
+
// A fence is only ever made by fenceFor (agent-fence.ts), which marks it required: one without the mark was put
|
|
59
|
+
// together some other way, and would run bare wherever no OS fence applies. Nothing runs.
|
|
60
|
+
if (fence.length > 0 && bag.confinement !== "required")
|
|
61
|
+
throw new ContainmentRefusal("This process was given a fence that isn't marked required, so it was not started.");
|
|
62
|
+
if (fence.length > 0 && (macosFenceAvailable() || linuxFenceAvailable())) {
|
|
63
|
+
try {
|
|
64
|
+
return sandboxedSpawn(file, args, fence, bag.fenceGrants, bag.cwd, bag.temp);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
throw new ContainmentRefusal(`The sandbox couldn't be made, so nothing was started: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
if (bag.confinement === "required") {
|
|
62
71
|
throw new ContainmentRefusal(fence.length === 0
|
|
63
72
|
? "This process must run inside the agent fence, and it was given none."
|
|
64
73
|
: "This machine can't confine a build (no macOS sandbox, no working bubblewrap), so nothing was started.");
|
|
65
74
|
}
|
|
66
|
-
return { file, args: [...args] };
|
|
75
|
+
return { file, args: [...args], env: {} };
|
|
76
|
+
}
|
|
77
|
+
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
78
|
+
export function withSandboxEnv(env, added) {
|
|
79
|
+
return Object.keys(added).length === 0 ? env : { ...(env ?? process.env), ...added };
|
|
67
80
|
}
|
|
68
81
|
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
69
82
|
* must reap exactly this child before answering. */
|
|
@@ -92,7 +105,9 @@ function rejectedChild(error, child) {
|
|
|
92
105
|
*/
|
|
93
106
|
function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
94
107
|
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
95
|
-
|
|
108
|
+
const sandboxed = confined(file, args, { ...bag, cwd: spawnOptions.cwd, temp: spawnOptions.env?.["TMPDIR"] });
|
|
109
|
+
({ file, args } = sandboxed);
|
|
110
|
+
spawnOptions = { ...spawnOptions, env: withSandboxEnv(spawnOptions.env, sandboxed.env) };
|
|
96
111
|
// The policy refusal comes first: nothing is reserved, recorded or
|
|
97
112
|
// spawned for a spawn that cannot be contained as required.
|
|
98
113
|
const effective = contain ? currentContainment() : null;
|
|
@@ -553,7 +568,7 @@ export function run(file, args, options = {}) {
|
|
|
553
568
|
// whose confinement is required and can't be wrapped refuses before anything runs.
|
|
554
569
|
let wrapped;
|
|
555
570
|
try {
|
|
556
|
-
wrapped = confined(file, args, options);
|
|
571
|
+
wrapped = confined(file, args, { ...options, cwd, temp: childEnv?.["TMPDIR"] });
|
|
557
572
|
}
|
|
558
573
|
catch (error) {
|
|
559
574
|
if (!(error instanceof ContainmentRefusal))
|
|
@@ -568,7 +583,7 @@ export function run(file, args, options = {}) {
|
|
|
568
583
|
encoding: "utf8",
|
|
569
584
|
shell: false,
|
|
570
585
|
windowsHide: true,
|
|
571
|
-
...(
|
|
586
|
+
...((env => env === undefined ? {} : { env }))(withSandboxEnv(childEnv, wrapped.env)),
|
|
572
587
|
}, (error, stdout, stderr) => {
|
|
573
588
|
if (error === null) {
|
|
574
589
|
resolve({ result: { code: 0, stdout, stderr, timedOut: false, notFound: false }, transient: false });
|
|
@@ -1373,7 +1388,7 @@ function processRows() {
|
|
|
1373
1388
|
* wrapper around it), down to the agent's own process (the first whose command is the agent's launch, `names`), then
|
|
1374
1389
|
* every live, non-zombie process beneath it, with its group. The agent itself and any wrapper are never counted.
|
|
1375
1390
|
*/
|
|
1376
|
-
function liveUnderAgent(root, names) {
|
|
1391
|
+
function liveUnderAgent(root, names, rootIsWrapper = false) {
|
|
1377
1392
|
const rows = processRows();
|
|
1378
1393
|
if (rows === null)
|
|
1379
1394
|
return null;
|
|
@@ -1381,9 +1396,10 @@ function liveUnderAgent(root, names) {
|
|
|
1381
1396
|
for (const row of rows)
|
|
1382
1397
|
children.set(row.parent, [...(children.get(row.parent) ?? []), row]);
|
|
1383
1398
|
const self = rows.find(row => row.pid === root);
|
|
1384
|
-
const named = (row) =>
|
|
1399
|
+
const named = (row) => agentNamed(row, names);
|
|
1385
1400
|
// The agent: the root when it is the agent (macOS sandbox-exec execs in place), else the first named descendant.
|
|
1386
|
-
|
|
1401
|
+
// On Linux the root is the sandbox launch helper (sandbox-launch.ts, itself node): never taken for the agent.
|
|
1402
|
+
let agent = !rootIsWrapper && self !== undefined && named(self) ? root : null;
|
|
1387
1403
|
const pending = [...(children.get(root) ?? [])];
|
|
1388
1404
|
while (agent === null && pending.length > 0) {
|
|
1389
1405
|
const row = pending.shift();
|
|
@@ -1404,6 +1420,27 @@ function liveUnderAgent(root, names) {
|
|
|
1404
1420
|
}
|
|
1405
1421
|
return found;
|
|
1406
1422
|
}
|
|
1423
|
+
/** The program a Linux process runs (its /proc exe), or null: elsewhere, or unreadable. */
|
|
1424
|
+
function linuxProgramOf(pid) {
|
|
1425
|
+
if (process.platform !== "linux")
|
|
1426
|
+
return null;
|
|
1427
|
+
try {
|
|
1428
|
+
return readlinkSync(`/proc/${pid}/exe`).replace(/ \(deleted\)$/, "").split("/").pop() || null;
|
|
1429
|
+
}
|
|
1430
|
+
catch {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Whether a listed process is the agent's launch (`names`): by its listed name, or on Linux by the program it runs,
|
|
1436
|
+
* since a listing there shows the main thread's name, which Node 24 sets to "MainThread".
|
|
1437
|
+
*/
|
|
1438
|
+
export function agentNamed(row, names, programOf = linuxProgramOf) {
|
|
1439
|
+
if (names.some(name => row.name === name || row.name.endsWith(`/${name}`)))
|
|
1440
|
+
return true;
|
|
1441
|
+
const program = programOf(row.pid);
|
|
1442
|
+
return program !== null && names.includes(program);
|
|
1443
|
+
}
|
|
1407
1444
|
/** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
|
|
1408
1445
|
export const ACP_SHUTDOWN_MS = { close: 2_000, eof: 5_000, term: 5_000 };
|
|
1409
1446
|
/** How long each handshake step (initialize, session/new, choosing the model) may take before the agent is stopped. */
|
|
@@ -1612,7 +1649,7 @@ export function runAcpSession(file, args, options = {}) {
|
|
|
1612
1649
|
forceKill();
|
|
1613
1650
|
});
|
|
1614
1651
|
},
|
|
1615
|
-
liveDescendants: () => process.platform === "win32" ? null : exited() || child.pid === undefined ? [] : liveUnderAgent(child.pid, agentNames),
|
|
1652
|
+
liveDescendants: () => process.platform === "win32" ? null : exited() || child.pid === undefined ? [] : liveUnderAgent(child.pid, agentNames, child.spawnargs.some(arg => arg.endsWith("sandbox-launch.js"))),
|
|
1616
1653
|
});
|
|
1617
1654
|
}
|
|
1618
1655
|
catch { /* the caller's own failure */ }
|
package/dist/flow-actions.d.ts
CHANGED
|
@@ -75,4 +75,5 @@ export declare function toolWaiting(store: Store, stage: FlowStage, repo: string
|
|
|
75
75
|
export declare function useTool(store: Store, stage: FlowStage, card: FlowCardRow, repo: string, io: {
|
|
76
76
|
callTool?: ToolCaller;
|
|
77
77
|
toolHome?: string;
|
|
78
|
+
fenceAvailable?: () => boolean;
|
|
78
79
|
}): Promise<ActionOutcome>;
|
package/dist/flow-actions.js
CHANGED
|
@@ -25,7 +25,7 @@ import { googleAccessToken, googleConnected } from "./google-mail.js";
|
|
|
25
25
|
import { ADDRESS, readEmailSettings } from "./email-settings.js";
|
|
26
26
|
import { readFlowSecrets, scrubSecrets } from "./flow-secrets.js";
|
|
27
27
|
export { readEmailSettings, saveEmailSettings } from "./email-settings.js";
|
|
28
|
-
import { callProjectTool, projectToolsOf, readToolSecrets } from "./project-tools.js";
|
|
28
|
+
import { callProjectTool, projectToolsOf, readToolSecrets, toolServerFor } from "./project-tools.js";
|
|
29
29
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
30
30
|
const clip = (text, cap) => text.length <= cap ? text : `${text.slice(0, cap - 1)}…`;
|
|
31
31
|
const blank = (text) => redactSecretAssignments(redactSecretLines(text, scanForSecrets(text)));
|
|
@@ -189,7 +189,8 @@ export async function useTool(store, stage, card, repo, io) {
|
|
|
189
189
|
const text = { title: card.title, description: card.description, note: card.note, outputs: card.outputs };
|
|
190
190
|
const args = fillJson(JSON.parse(call.args), one => fillFlowText(one, text));
|
|
191
191
|
const values = readToolSecrets(repo, tool.name, io.toolHome);
|
|
192
|
-
const answer = await (io.callTool ?? ((spec, secrets, name, given) => callProjectTool(spec, secrets, name, given, { timeoutMs: 120_000, omitEnv: ALL_CREDENTIAL_ENV
|
|
192
|
+
const answer = await (io.callTool ?? ((spec, secrets, name, given) => callProjectTool(spec, secrets, name, given, { timeoutMs: 120_000, omitEnv: ALL_CREDENTIAL_ENV,
|
|
193
|
+
server: toolServerFor(store, repo, { ...(io.toolHome === undefined ? {} : { home: io.toolHome }), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }) }) })))(tool.spec, values, call.name, args);
|
|
193
194
|
if (!answer.ok)
|
|
194
195
|
return { state: "retry", said: `${call.server}: ${answer.problem}` };
|
|
195
196
|
const said = blank(scrub(clip(answer.text, 64_000), values));
|
package/dist/flow-code.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Runner } from "./backend.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type FencePolicy, type LaunchFence } from "./agent-fence.js";
|
|
3
|
+
import type { FlowScriptRow, Store } from "./store.js";
|
|
3
4
|
import { projectFile } from "./project-file.js";
|
|
4
5
|
export { projectFile };
|
|
5
6
|
export { LANGUAGE_WORDS, SCRIPT_LANGUAGES, type ScriptLanguage } from "./flows.js";
|
|
@@ -17,7 +18,9 @@ export type CodeRun = {
|
|
|
17
18
|
secrets: Record<string, string>;
|
|
18
19
|
scratch: string;
|
|
19
20
|
shell: Runner;
|
|
20
|
-
|
|
21
|
+
/** Where it runs from (flowLaunch): its fence, and the launch folder its input and an inline script are written to;
|
|
22
|
+
* null only where there is nothing to fence (a store with no database file). */
|
|
23
|
+
launch: FlowLaunch | null;
|
|
21
24
|
};
|
|
22
25
|
/** `printed`: everything it printed (secrets blanked), for a trigger to read items from; `output` is what a card keeps. */
|
|
23
26
|
export type CodeResult = {
|
|
@@ -49,3 +52,29 @@ export declare function cardsFromOutput(stdout: string): {
|
|
|
49
52
|
title: string;
|
|
50
53
|
description: string | null;
|
|
51
54
|
}[];
|
|
55
|
+
/**
|
|
56
|
+
* Where one flow launch runs (docs/plans/sandbox-parity.md C1): the fence `fenceFor` makes for a flow's setup, script
|
|
57
|
+
* or trigger, around `folder` (a copy of the card's work, bound to its project, or a clean folder), with a launch folder
|
|
58
|
+
* of its own for its input, an inline script and its temp files; writes as the project's writes setting allows (its
|
|
59
|
+
* folder, the copy's Git data, its project's caches); the scratch pool denied around its own folder; and its network
|
|
60
|
+
* open, as flows' scripts have always had it (they reach the services their secrets are for; no approval seals a
|
|
61
|
+
* network for them). The fence is required: where this machine can't confine it, it runs only when an instance operator
|
|
62
|
+
* allowed unconfined agents, and otherwise nothing runs and the card says why.
|
|
63
|
+
*/
|
|
64
|
+
export type FlowLaunch = {
|
|
65
|
+
folder: string;
|
|
66
|
+
env: Record<string, string>;
|
|
67
|
+
fence?: LaunchFence;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
};
|
|
70
|
+
export declare function flowLaunch(store: Store, options: {
|
|
71
|
+
repo: string;
|
|
72
|
+
folder: string;
|
|
73
|
+
scratch: string;
|
|
74
|
+
copy: boolean;
|
|
75
|
+
available?: () => boolean;
|
|
76
|
+
policy?: FencePolicy;
|
|
77
|
+
home?: string;
|
|
78
|
+
}): FlowLaunch | null | {
|
|
79
|
+
problem: string;
|
|
80
|
+
};
|
package/dist/flow-code.js
CHANGED
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
* keys are out of reach), under a time limit, with the setup's plain
|
|
16
16
|
* environment rather than this process's.
|
|
17
17
|
*/
|
|
18
|
-
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { randomBytes } from "node:crypto";
|
|
20
|
-
import {
|
|
20
|
+
import { homedir, tmpdir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { buildCacheEnv, buildCacheFolder, checkLaunchRoot, fenceFor, linuxFenceAvailable, macosFenceAvailable, runFolders, taskBoundary, trustedWorktreeBinding, writeAllowlist } from "./agent-fence.js";
|
|
23
|
+
import { readDenyPath } from "./project-sandbox.js";
|
|
24
|
+
import { poolRootOf } from "./worktree.js";
|
|
21
25
|
import { SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
22
26
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
23
27
|
import { scrubSecrets } from "./flow-secrets.js";
|
|
@@ -51,7 +55,10 @@ export async function runCode(run) {
|
|
|
51
55
|
const language = script.language;
|
|
52
56
|
const tag = randomBytes(6).toString("hex");
|
|
53
57
|
mkdirSync(run.scratch, { recursive: true });
|
|
54
|
-
|
|
58
|
+
// The input and an inline script live in the launch's own folder, the one place beside its working folder its fence
|
|
59
|
+
// gives back (every other launch folder, and every other flow copy in the scratch pool, stay out of reach).
|
|
60
|
+
const files = run.launch?.folder ?? run.scratch;
|
|
61
|
+
const inputFile = join(files, `flow-input-${tag}.json`);
|
|
55
62
|
let scriptFile = null;
|
|
56
63
|
try {
|
|
57
64
|
writeFileSync(inputFile, JSON.stringify(run.input), { mode: 0o600 });
|
|
@@ -61,7 +68,7 @@ export async function runCode(run) {
|
|
|
61
68
|
return { state: "failed", exitCode: null, output: "", printed: "", goTo: null, log: "", said: `${script.name} runs ${script.file}, which isn't a file in the project.` };
|
|
62
69
|
}
|
|
63
70
|
else {
|
|
64
|
-
const own = join(
|
|
71
|
+
const own = join(files, `flow-script-${tag}.${EXTENSION[language]}`);
|
|
65
72
|
writeFileSync(own, `${script.body}\n`, { mode: 0o700 });
|
|
66
73
|
chmodSync(own, 0o700);
|
|
67
74
|
scriptFile = own;
|
|
@@ -71,7 +78,7 @@ export async function runCode(run) {
|
|
|
71
78
|
// first without breaking anything. Only our own paths are in the command; the card is only data.
|
|
72
79
|
const ran = await run.shell("/bin/sh", ["-c", 'exec "$@" < "$FLOW_INPUT"', "flow-script", runtime.file, ...runtime.args], {
|
|
73
80
|
cwd: run.cwd, timeoutMs: script.timeoutMinutes * 60_000, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
|
|
74
|
-
env: { ...run.env, ...run.secrets, FLOW_INPUT: inputFile },
|
|
81
|
+
env: { ...run.env, ...run.secrets, ...(run.launch?.env ?? {}), FLOW_INPUT: inputFile }, ...(run.launch?.fence ?? {}),
|
|
75
82
|
});
|
|
76
83
|
const printed = codeOutput(ran.stdout, run.secrets);
|
|
77
84
|
const all = scrubSecrets(ran.stdout.slice(0, 1_000_000), run.secrets);
|
|
@@ -148,3 +155,48 @@ export function cardsFromOutput(stdout) {
|
|
|
148
155
|
return asItem(trimmed);
|
|
149
156
|
}).filter((one) => one !== null);
|
|
150
157
|
}
|
|
158
|
+
export function flowLaunch(store, options) {
|
|
159
|
+
const databaseFile = store.databaseFile();
|
|
160
|
+
if (databaseFile === null || databaseFile === "" || databaseFile === ":memory:")
|
|
161
|
+
return null;
|
|
162
|
+
const available = options.available ?? (() => macosFenceAvailable() || linuxFenceAvailable());
|
|
163
|
+
const fenced = available();
|
|
164
|
+
// Asked first: a launch with no fence of its own (an instance operator allowed it, as inside another sandbox) makes
|
|
165
|
+
// its folder in the temp folder it was given, never among the fenced launches' (which an outer sandbox may hold).
|
|
166
|
+
if (!fenced && !store.unfencedAgentsAllowed()) {
|
|
167
|
+
return { problem: "This machine can't confine the flow's script (no macOS sandbox, no working bubblewrap), so it didn't run. Install bubblewrap, socat and ripgrep (Linux), or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`." };
|
|
168
|
+
}
|
|
169
|
+
let own;
|
|
170
|
+
try {
|
|
171
|
+
own = realpathSync(mkdtempSync(join(fenced ? checkLaunchRoot() : tmpdir(), "so-cmd-")));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return { problem: `The flow's launch folder couldn't be made, so nothing ran: ${error instanceof Error ? error.message : String(error)}` };
|
|
175
|
+
}
|
|
176
|
+
const dispose = () => rmSync(own, { recursive: true, force: true });
|
|
177
|
+
const temp = join(own, "tmp");
|
|
178
|
+
mkdirSync(temp, { mode: 0o700 });
|
|
179
|
+
const env = { TMPDIR: temp, TMP: temp, TEMP: temp };
|
|
180
|
+
if (!fenced)
|
|
181
|
+
return { folder: own, env, dispose };
|
|
182
|
+
try {
|
|
183
|
+
const poolRoots = [join(dirname(databaseFile), "worktrees"), options.scratch, ...store.listWorktrees().map(record => poolRootOf(record.path, record.repo))];
|
|
184
|
+
// Its scratch folder is a pool root (so another flow's copy is out of reach) and sealed: never read, at any policy.
|
|
185
|
+
// A copy is held to its project by the plane's own record of the repository, never what the copy says.
|
|
186
|
+
const binding = options.copy ? trustedWorktreeBinding(options.repo, options.folder, poolRoots) : null;
|
|
187
|
+
// The policy its flow sealed; nothing sealed is v1 (the home configuration denied), never a wider one implicitly.
|
|
188
|
+
const home = options.home ?? homedir();
|
|
189
|
+
const boundary = taskBoundary({ worktree: options.folder, provider: "command", home, binding, policy: options.policy ?? 1 });
|
|
190
|
+
const cache = buildCacheFolder(options.repo, home);
|
|
191
|
+
const allowed = store.projectWrites(options.repo) !== "workspace" ? null : writeAllowlist({ provider: "command", home, worktree: options.folder, binding, launchFolder: own, cache });
|
|
192
|
+
// The project's read denies apply to flows as to builds (read live: a flow seals no list, and these only narrow).
|
|
193
|
+
const readDeny = store.projectSandbox(options.repo).readDeny.map(one => readDenyPath(one, home));
|
|
194
|
+
const fence = fenceFor("flow", { databaseFile, worktree: options.folder, home, boundary, readDeny, policy: options.policy ?? 1, scratchRoots: [options.scratch], binding, runFolders: runFolders(own), poolRoots, extra: allowed?.deny ?? [],
|
|
195
|
+
writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, network: { mode: "open" } });
|
|
196
|
+
return { folder: own, env: { ...env, ...(allowed === null ? {} : buildCacheEnv(cache)) }, fence, dispose };
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
dispose();
|
|
200
|
+
return { problem: "The flow's copy couldn't be bound to its project, so nothing ran." };
|
|
201
|
+
}
|
|
202
|
+
}
|
package/dist/flow-steps.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type StepIo = {
|
|
|
14
14
|
dir: string | null;
|
|
15
15
|
/** Where temporary copies of a card's work are made. */
|
|
16
16
|
scratch: string;
|
|
17
|
+
/** Whether this machine can confine a script (tests only; default: the macOS sandbox or bubblewrap is here). */
|
|
18
|
+
fenceAvailable?: () => boolean;
|
|
19
|
+
/** The home folder a launch is fenced around (tests only; default: this user's). */
|
|
20
|
+
home?: string;
|
|
17
21
|
evidenceRoot?: string;
|
|
18
22
|
/** The branch a card with no result yet is checked against. */
|
|
19
23
|
base: string;
|
package/dist/flow-steps.js
CHANGED
|
@@ -35,8 +35,7 @@ import { JEV_MODEL, askJev, readJevAnswers, sortLog, sortRequest, sortState, sor
|
|
|
35
35
|
import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft } from "./flow-draft.js";
|
|
36
36
|
import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
|
|
37
37
|
import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
|
|
38
|
-
import { cleanFolder, runCode } from "./flow-code.js";
|
|
39
|
-
import { agentFence } from "./agent-fence.js";
|
|
38
|
+
import { cleanFolder, flowLaunch, runCode } from "./flow-code.js";
|
|
40
39
|
import { readProviderKey } from "./keys.js";
|
|
41
40
|
import { replyInChannel } from "./chat-inbox.js";
|
|
42
41
|
import { recordSent, threadOf } from "./flow-replies.js";
|
|
@@ -362,13 +361,16 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
362
361
|
if (added.code !== 0)
|
|
363
362
|
return { state: "retry", said: `Couldn't make a copy of${where}: ${added.stderr.trim().split("\n")[0]?.slice(0, 160) ?? "git refused"}.` };
|
|
364
363
|
}
|
|
364
|
+
const launch = flowLaunch(store, { repo: flow.repo, folder: path, scratch: io.scratch, copy, policy: store.flowFencePolicy(flow.id), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
|
|
365
365
|
try {
|
|
366
|
-
|
|
366
|
+
if (launch !== null && "problem" in launch)
|
|
367
|
+
return { state: "failed", said: launch.problem };
|
|
367
368
|
let log = "";
|
|
368
369
|
const setup = copy ? store.liveWorktreeSetup(flow.repo) : null;
|
|
369
370
|
if (setup !== null) {
|
|
370
371
|
const shell = approvedCommandShell(setup.command);
|
|
371
|
-
const prepared = await io.shell(shell.file, shell.args, { cwd: path, timeoutMs: setup.timeoutMs, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
|
|
372
|
+
const prepared = await io.shell(shell.file, shell.args, { cwd: path, timeoutMs: setup.timeoutMs, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
|
|
373
|
+
...(launch === null ? {} : { env: launch.env, ...(launch.fence ?? {}) }) });
|
|
372
374
|
log += `$ ${setup.command}\n${prepared.stdout}${prepared.stderr}\n`;
|
|
373
375
|
if (prepared.code !== 0)
|
|
374
376
|
return { state: "failed", said: `The project's setup failed before ${script.name} ran (exit ${prepared.code}).\n${tail(`${prepared.stdout}\n${prepared.stderr}`)}`, log, exitCode: prepared.code };
|
|
@@ -383,7 +385,7 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
383
385
|
answers: (stage.routes ?? []).map(one => one.answer),
|
|
384
386
|
};
|
|
385
387
|
const env = { FLOW_NAME: flow.name, FLOW_CARD_ID: String(card.id), FLOW_CARD_TITLE: card.title, FLOW_COMMIT: commit ?? "", FLOW_SCRIPT: script.name, FLOW_PROJECT: flow.repo };
|
|
386
|
-
const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell,
|
|
388
|
+
const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell, launch });
|
|
387
389
|
log += ran.log;
|
|
388
390
|
const output = ran.output === "" ? undefined : ran.output;
|
|
389
391
|
// Where it ran goes at the end of the first line: "run-tests failed (exit 1) on main."
|
|
@@ -398,6 +400,8 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
398
400
|
return { state: "passed", said: `${script.name} ran${where} and picked ${route.answer}.`, log, exitCode: 0, to: route.to, ...(output === undefined ? {} : { output }) };
|
|
399
401
|
}
|
|
400
402
|
finally {
|
|
403
|
+
if (launch !== null && !("problem" in launch))
|
|
404
|
+
launch.dispose();
|
|
401
405
|
if (copy)
|
|
402
406
|
await io.git("git", ["-C", flow.repo, "worktree", "remove", "--force", path], { timeoutMs: 60_000 }).catch(() => undefined);
|
|
403
407
|
rmSync(path, { recursive: true, force: true });
|
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -141,6 +141,10 @@ export type TriggerIo = {
|
|
|
141
141
|
/** v90: how a schedule's script runs (default: the same runner as gh) and where (default: beside the database). */
|
|
142
142
|
shell?: Runner;
|
|
143
143
|
scratch?: string;
|
|
144
|
+
/** Whether this machine can confine a script (tests only; default: the macOS sandbox or bubblewrap is here). */
|
|
145
|
+
fenceAvailable?: () => boolean;
|
|
146
|
+
/** The home folder a launch is fenced around (tests only; default: this user's). */
|
|
147
|
+
home?: string;
|
|
144
148
|
};
|
|
145
149
|
export type TriggerPass = {
|
|
146
150
|
added: number;
|
package/dist/flow-triggers.js
CHANGED
|
@@ -859,21 +859,26 @@ async function fireScript(store, trigger, config, now, io, scheduled) {
|
|
|
859
859
|
if (missing.length > 0)
|
|
860
860
|
return done(false, `${script.name} needs the secret${missing.length === 1 ? "" : "s"} ${missing.join(", ")}. Save ${missing.length === 1 ? "it" : "them"} on the flow first.`);
|
|
861
861
|
// Loaded when a script first runs: the runner reaches the builder's modules, which reach back to these triggers.
|
|
862
|
-
const { cardsFromOutput, cleanFolder, runCode } = await import("./flow-code.js");
|
|
863
|
-
const { agentFence } = await import("./agent-fence.js");
|
|
862
|
+
const { cardsFromOutput, cleanFolder, flowLaunch, runCode } = await import("./flow-code.js");
|
|
864
863
|
const scratch = io.scratch ?? join(io.dir ?? tmpdir(), "flow-scratch");
|
|
865
864
|
const folder = cleanFolder(scratch, `flow-trigger-${trigger.id}`);
|
|
866
865
|
// Its cards run on their own only if it ran the very code a grant signed: pinned just before and just after the run.
|
|
867
866
|
const pin = scheduled && grantSignsScriptOf(store, flow.id, trigger.id, now) ? () => scriptProvenanceOf(store, flow.repo, script.name) : null;
|
|
868
867
|
const before = pin === null ? null : await pin();
|
|
869
868
|
let ran;
|
|
869
|
+
// A full boundary around its clean folder, though it has no checkout (flow-code.ts flowLaunch).
|
|
870
|
+
const launch = flowLaunch(store, { repo: flow.repo, folder, scratch, copy: false, policy: store.flowFencePolicy(flow.id), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
|
|
870
871
|
try {
|
|
872
|
+
if (launch !== null && "problem" in launch)
|
|
873
|
+
return done(false, launch.problem);
|
|
871
874
|
ran = await runCode({ script, cwd: folder, root: flow.repo, scratch, shell: io.shell ?? io.gh,
|
|
872
875
|
input: { flow: { id: flow.id, name: flow.name }, trigger: trigger.id, lastRun: trigger.lastAt }, secrets: Object.fromEntries((config.secrets ?? []).map(name => [name, saved[name]])),
|
|
873
876
|
env: { FLOW_NAME: flow.name, FLOW_TRIGGER_ID: String(trigger.id), FLOW_PROJECT: flow.repo, FLOW_LAST_RUN: trigger.lastAt ?? "" },
|
|
874
|
-
|
|
877
|
+
launch });
|
|
875
878
|
}
|
|
876
879
|
finally {
|
|
880
|
+
if (launch !== null && !("problem" in launch))
|
|
881
|
+
launch.dispose();
|
|
877
882
|
rmSync(folder, { recursive: true, force: true });
|
|
878
883
|
}
|
|
879
884
|
if (ran.state === "failed")
|
package/dist/invoke.d.ts
CHANGED
|
@@ -21,7 +21,9 @@ import { type VersionProbe } from "./attest.js";
|
|
|
21
21
|
import type { Store } from "./store.js";
|
|
22
22
|
import type { RunOptions } from "./exec.js";
|
|
23
23
|
import type { Runner } from "./worktree.js";
|
|
24
|
-
import { type
|
|
24
|
+
import { type NetworkFence, type SignInGrants } from "./agent-fence.js";
|
|
25
|
+
import { type CheckAccess } from "./check-access.js";
|
|
26
|
+
import { type WritesMode, type LaunchSandbox, type FenceGrants } from "./agent-fence.js";
|
|
25
27
|
import { type NetworkProfile } from "./egress.js";
|
|
26
28
|
export type { ProviderRunner } from "./provider.js";
|
|
27
29
|
export type ProviderUsage = {
|
|
@@ -109,6 +111,7 @@ export declare function invokeAgent(store: Store, runId: number, spec: AgentSpec
|
|
|
109
111
|
accumulateUsage?: boolean;
|
|
110
112
|
network?: LaunchNetwork;
|
|
111
113
|
writes?: WritesMode;
|
|
114
|
+
sandbox?: LaunchSandbox;
|
|
112
115
|
}): Promise<InvokeResult>;
|
|
113
116
|
/**
|
|
114
117
|
* A build's setup and check commands, run in its fence (arc 2): the same denies as its agent's (Toolroll's state,
|
|
@@ -117,11 +120,55 @@ export declare function invokeAgent(store: Store, runId: number, spec: AgentSpec
|
|
|
117
120
|
* Their network stays as it was. A machine without an OS fence runs them as before; a fence that can't be prepared
|
|
118
121
|
* (a checkout whose binding no longer proves) runs nothing.
|
|
119
122
|
*/
|
|
120
|
-
export declare function fencedCommands(store: Store, runId: number, writes: WritesMode, inner: Runner, available?: () => boolean): Runner;
|
|
123
|
+
export declare function fencedCommands(store: Store, runId: number, writes: WritesMode, inner: Runner, available?: () => boolean, access?: CheckAccess, network?: NetworkProfile, sandbox?: LaunchSandbox): Runner;
|
|
121
124
|
/**
|
|
122
125
|
* A project's checks run in a checkout of a result's commit made for them (result-follow-ups.ts, batch-checks.ts), in
|
|
123
126
|
* the same fence as a build's commands: Toolroll's state, credentials, other projects and runs denied; under the
|
|
124
127
|
* project's writes setting, only that checkout, its Git data, its own temp folder, the project's caches and its check
|
|
125
128
|
* database writable. Where nothing can confine them, they run only when an instance operator allowed unconfined agents.
|
|
126
129
|
*/
|
|
127
|
-
export declare function fencedCheckoutCommands(store: Store, repo: string, checkout: string, inner: Runner, available?: () => boolean): Runner;
|
|
130
|
+
export declare function fencedCheckoutCommands(store: Store, repo: string, checkout: string, inner: Runner, available?: () => boolean, access?: CheckAccess, network?: NetworkProfile, sandbox?: LaunchSandbox): Runner;
|
|
131
|
+
/**
|
|
132
|
+
* A `sign-ins` check's launch (check-access.ts, sandbox parity C10): its exact sign-in files, read by the check itself
|
|
133
|
+
* through its sandbox (agent-fence.ts signInGrants: nothing privileged reads or copies one), its own transcript folder
|
|
134
|
+
* and Codex's session and log folders; Codex, gh and XDG pointed at the real folders; and the network the check runs
|
|
135
|
+
* under: open, or its allowlist through a proxy of its own (on Linux, relayed in by the sandbox runtime). Nothing about it is
|
|
136
|
+
* started unless all of it is.
|
|
137
|
+
*/
|
|
138
|
+
export declare function signInsLaunch(store: Store, own: string, temp: string, checkout: string, policy: NetworkProfile, gitPaths: readonly string[], platform?: NodeJS.Platform): Promise<{
|
|
139
|
+
grants: SignInGrants;
|
|
140
|
+
network: NetworkFence;
|
|
141
|
+
env: Record<string, string>;
|
|
142
|
+
close(): Promise<void>;
|
|
143
|
+
} | {
|
|
144
|
+
problem: string;
|
|
145
|
+
}>;
|
|
146
|
+
/** How a check at `access` actually runs here: unsandboxed where no OS fence exists (and an operator allowed that), else
|
|
147
|
+
* as asked. Recorded on every check's receipt, so a check that ran without a sandbox is never shown as sandboxed. */
|
|
148
|
+
export declare function actualCheckAccess(store: Store, access: CheckAccess): CheckAccess;
|
|
149
|
+
/**
|
|
150
|
+
* A check of saved results, in their own checkout (batch-checks.ts, result-follow-ups.ts), worked out right before each
|
|
151
|
+
* command: never wider than any of those results' own checks ran, nor than the project allows now; under sign-ins, the
|
|
152
|
+
* narrowest of their sealed check networks and the project's current one. A result with no sealed network runs it
|
|
153
|
+
* sandboxed.
|
|
154
|
+
*/
|
|
155
|
+
export declare function checkoutLaunchOf(store: Store, repo: string, runIds: readonly number[], platform?: NodeJS.Platform): {
|
|
156
|
+
access: CheckAccess;
|
|
157
|
+
network: NetworkProfile | undefined;
|
|
158
|
+
};
|
|
159
|
+
/** The widest level a check's commands actually ran at (what its receipt says). */
|
|
160
|
+
export declare const widestRan: (levels: readonly CheckAccess[]) => CheckAccess;
|
|
161
|
+
/** A check log's line for how its commands ran, when wider than sandboxed (shown with the log). */
|
|
162
|
+
export declare const ranAtLine: (level: CheckAccess) => string | null;
|
|
163
|
+
/** A saved result's check command runner (batch-checks.ts, result-follow-ups.ts): its level and network worked out right
|
|
164
|
+
* now (checkoutLaunchOf), and how it actually runs told to `ran`. */
|
|
165
|
+
export declare function checkoutRunner(store: Store, repo: string, checkout: string, inner: Runner, runIds: readonly number[], ran: (level: CheckAccess) => void, platform?: NodeJS.Platform): Runner;
|
|
166
|
+
/**
|
|
167
|
+
* A saved result's checks' sandbox terms (sandbox parity C9), never wider than any of those results' own approvals nor
|
|
168
|
+
* the project now: v2 only when every result's approval sealed v2; every read deny any of them sealed or the project
|
|
169
|
+
* lists now; mach services only those all of them sealed and the project still lists. A result with
|
|
170
|
+
* no readable approval counts as v1 with nothing listed.
|
|
171
|
+
*/
|
|
172
|
+
export declare function checkoutSandboxOf(store: Store, repo: string, runIds: readonly number[]): LaunchSandbox;
|
|
173
|
+
/** The digest of a launch's whole fence (its denied paths and grants, canonical), as stamped on its run (C11). */
|
|
174
|
+
export declare function fenceDigest(fence: readonly string[], grants: FenceGrants): string;
|