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/project-tools.js
CHANGED
|
@@ -24,11 +24,15 @@ import { createHash } from "node:crypto";
|
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
26
|
import { homedir, tmpdir } from "node:os";
|
|
27
|
+
import { realpathSync } from "node:fs";
|
|
27
28
|
import { dirname, isAbsolute, join } from "node:path";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { scanForSecrets } from "./evidence.js";
|
|
30
31
|
import { namedPath } from "./names.js";
|
|
31
32
|
import { startToolBroker } from "./tool-broker.js";
|
|
33
|
+
import { buildCacheFolder, fenceFor, linuxFenceAvailable, macosFenceAvailable, runFolders } from "./agent-fence.js";
|
|
34
|
+
import { confined, ContainmentRefusal } from "./exec.js";
|
|
35
|
+
import { poolRootOf } from "./worktree.js";
|
|
32
36
|
import { isAcpProvider } from "./acp-agents.js";
|
|
33
37
|
import { SecretStoreError, destroyOwnFile, prepareSecretFolder, registerSecretFolder, pathIn, readOwnFile, readSealed, removeValue, secretBackend, secretFolder, secretStorage, serviceOf, withSecretLock, withSecretLockAsync, writeOwnFile, writeSealed } from "./tool-secret-store.js";
|
|
34
38
|
import { ACCESS, GRANTED, REFRESH, mintRunToken, mintsRunTokens, oneAtATime, runScope, scopeList, signedIn } from "./oauth-run-token.js";
|
|
@@ -507,7 +511,7 @@ function toolNames(result) {
|
|
|
507
511
|
return Array.isArray(tools) ? tools.map(one => one.name).filter((name) => typeof name === "string").slice(0, 200) : [];
|
|
508
512
|
}
|
|
509
513
|
/** Start the server once, ask for its tools, and stop it: the proof a build can use it. */
|
|
510
|
-
export async function testTool(spec, values, options
|
|
514
|
+
export async function testTool(spec, values, options) {
|
|
511
515
|
const answer = await exchange(spec, values, { method: "tools/list", params: {}, doing: "listing its tools" }, options);
|
|
512
516
|
return answer.ok ? { ok: true, tools: toolNames(answer.result) } : answer;
|
|
513
517
|
}
|
|
@@ -516,7 +520,7 @@ export async function testTool(spec, values, options = {}) {
|
|
|
516
520
|
* its description, what input it takes, and whether it says it only reads.
|
|
517
521
|
* A subagent is offered these, each under its own rule.
|
|
518
522
|
*/
|
|
519
|
-
export async function listProjectToolActions(spec, values, options
|
|
523
|
+
export async function listProjectToolActions(spec, values, options) {
|
|
520
524
|
const answer = await exchange(spec, values, { method: "tools/list", params: {}, doing: "listing its tools" }, options);
|
|
521
525
|
return answer.ok ? { ok: true, actions: toolActions(answer.result) } : answer;
|
|
522
526
|
}
|
|
@@ -537,7 +541,7 @@ export function toolActions(result) {
|
|
|
537
541
|
* "Use a tool" step. The answer's text parts are kept; a tool that says it
|
|
538
542
|
* failed (isError) is an answer, not trouble reaching it.
|
|
539
543
|
*/
|
|
540
|
-
export async function callProjectTool(spec, values, name, args, options
|
|
544
|
+
export async function callProjectTool(spec, values, name, args, options) {
|
|
541
545
|
const answer = await exchange(spec, values, { method: "tools/call", params: { name, arguments: args }, doing: `using ${name}` }, options);
|
|
542
546
|
if (!answer.ok)
|
|
543
547
|
return answer;
|
|
@@ -552,9 +556,14 @@ function exchange(spec, values, request, options) {
|
|
|
552
556
|
if (missing.length > 0)
|
|
553
557
|
return Promise.resolve({ ok: false, problem: `Set ${missing.join(", ")} first.` });
|
|
554
558
|
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
555
|
-
|
|
559
|
+
if (spec.transport === "http")
|
|
560
|
+
return exchangeHttp(spec, values, timeoutMs, request);
|
|
561
|
+
// A local server starts only in the server fence its caller made (toolServerFor), never bare.
|
|
562
|
+
if ("problem" in options.server)
|
|
563
|
+
return Promise.resolve({ ok: false, problem: options.server.problem });
|
|
564
|
+
return exchangeStdio(spec, values, timeoutMs, options.env ?? process.env, options.omitEnv ?? [], request, options.server.fence);
|
|
556
565
|
}
|
|
557
|
-
function exchangeStdio(spec, values, timeoutMs, base, omit, request) {
|
|
566
|
+
function exchangeStdio(spec, values, timeoutMs, base, omit, request, fence) {
|
|
558
567
|
return new Promise(resolve => {
|
|
559
568
|
const env = { ...base };
|
|
560
569
|
for (const name of omit)
|
|
@@ -562,9 +571,14 @@ function exchangeStdio(spec, values, timeoutMs, base, omit, request) {
|
|
|
562
571
|
Object.assign(env, values);
|
|
563
572
|
let child;
|
|
564
573
|
try {
|
|
565
|
-
|
|
574
|
+
const launch = confined(spec.command, spec.args, { ...(fence ?? {}), temp: env["TMPDIR"] });
|
|
575
|
+
child = spawn(launch.file, launch.args, { env: { ...env, ...launch.env }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
566
576
|
}
|
|
567
577
|
catch (error) {
|
|
578
|
+
if (error instanceof ContainmentRefusal) {
|
|
579
|
+
resolve({ ok: false, problem: error.message });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
568
582
|
resolve({ ok: false, problem: `It could not start: ${error instanceof Error ? error.message : "unknown error"}.` });
|
|
569
583
|
return;
|
|
570
584
|
}
|
|
@@ -1004,7 +1018,8 @@ export async function testToolOf(store, repo, name, now, options = {}) {
|
|
|
1004
1018
|
const tool = projectToolsOf(store, repo).find(one => one.name === name);
|
|
1005
1019
|
if (tool === undefined)
|
|
1006
1020
|
return null;
|
|
1007
|
-
const
|
|
1021
|
+
const server = toolServerFor(store, repo, { ...(options.home === undefined ? {} : { home: options.home }), ...(options.available === undefined ? {} : { available: options.available }) });
|
|
1022
|
+
const outcome = await testTool(tool.spec, readToolSecrets(repo, name, options.home), { server, ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) });
|
|
1008
1023
|
const test = outcome.ok ? { at: now.toISOString(), ok: true, tools: outcome.tools, problem: null } : { at: now.toISOString(), ok: false, tools: [], problem: outcome.problem };
|
|
1009
1024
|
store.recordProjectToolTest(repo, name, JSON.stringify(test));
|
|
1010
1025
|
return test;
|
|
@@ -1093,3 +1108,24 @@ export function splitCommandLine(line) {
|
|
|
1093
1108
|
parts.push(current);
|
|
1094
1109
|
return parts;
|
|
1095
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* The server fence for a project tool started outside a build (docs/plans/sandbox-parity.md C2): discovery, its Test
|
|
1113
|
+
* button, a flow's "Use a tool" step, a subagent's call. As the broker's servers are fenced: Toolroll's state, every
|
|
1114
|
+
* other launch's folder and the checkout pools denied, and nothing a build writes (its project's caches) read, so no
|
|
1115
|
+
* byte a build wrote can load into a process that holds the tool's secrets. Required: where this machine can't confine
|
|
1116
|
+
* it, it doesn't start unless an instance operator allowed unconfined agents.
|
|
1117
|
+
*/
|
|
1118
|
+
export function toolServerFor(store, repo, options = {}) {
|
|
1119
|
+
const databaseFile = store.databaseFile();
|
|
1120
|
+
if (databaseFile === null || databaseFile === "" || databaseFile === ":memory:")
|
|
1121
|
+
return { fence: null };
|
|
1122
|
+
const available = options.available ?? (() => macosFenceAvailable() || linuxFenceAvailable());
|
|
1123
|
+
if (!available())
|
|
1124
|
+
return store.unfencedAgentsAllowed() ? { fence: null }
|
|
1125
|
+
: { problem: "This machine can't confine the tool's server (no macOS sandbox, no working bubblewrap), so it was not started." };
|
|
1126
|
+
const home = options.home ?? homedir();
|
|
1127
|
+
const poolRoots = [join(dirname(databaseFile), "worktrees"), ...store.listWorktrees().map(record => poolRootOf(record.path, record.repo))];
|
|
1128
|
+
const cache = buildCacheFolder(repo, home);
|
|
1129
|
+
const untrusted = existsSync(cache) ? [realpathSync(cache)] : [];
|
|
1130
|
+
return { fence: fenceFor("tool-server", { databaseFile, worktree: null, home, boundary: null, binding: null, runFolders: runFolders(null), poolRoots, extra: untrusted }) };
|
|
1131
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
/** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
|
|
32
32
|
* concluded `success` on the gated commit; src/ci-workflow.test.ts holds the workflow to this list. */
|
|
33
|
-
export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "Windows Job Object containment"];
|
|
33
|
+
export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment"];
|
|
34
34
|
/** The checks a release tag runs (.github/workflows/publish.yml), awaited before Homebrew moves. */
|
|
35
35
|
export declare const RELEASE_TAG_CHECKS: readonly ["node 22 · macos-latest", "node 24 · macos-latest", "publish"];
|
|
36
36
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
package/dist/release.js
CHANGED
|
@@ -37,6 +37,7 @@ export const REQUIRED_PR_CHECKS = [
|
|
|
37
37
|
"node 22 · ubuntu-latest",
|
|
38
38
|
"node 24 · ubuntu-latest",
|
|
39
39
|
"linux native containment · delegated cgroup v2",
|
|
40
|
+
"linux fence · sandbox-runtime",
|
|
40
41
|
"Windows Job Object containment",
|
|
41
42
|
];
|
|
42
43
|
/** The checks a release tag runs (.github/workflows/publish.yml), awaited before Homebrew moves. */
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
* Run checks runs the project's quick or full check on the result's exact
|
|
4
|
-
* commit, in a fresh checkout, after the build has finished. A person (or a
|
|
5
|
-
* pull request opening) asks; the ask is a ledger entry; a worker runs it
|
|
6
|
-
* once and seals its log and a receipt beside the result. A pass upgrades
|
|
7
|
-
* what the result's Checks row says; a failure stays visible. Nothing here
|
|
8
|
-
* rewrites the build's own check or reruns crew work.
|
|
9
|
-
*
|
|
10
|
-
* Add tests files a small task, unapproved like any other filing, to write
|
|
11
|
-
* tests for that change. */
|
|
12
|
-
import { fencedCheckoutCommands } from "./invoke.js";
|
|
1
|
+
import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
|
|
13
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
14
3
|
import { tmpdir } from "node:os";
|
|
15
4
|
import { join } from "node:path";
|
|
@@ -188,6 +177,8 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
188
177
|
const folder = mkdtempSync(join(deps.scratch ?? tmpdir(), "toolroll-checks-"));
|
|
189
178
|
const checkout = join(folder, "checkout");
|
|
190
179
|
const log = [];
|
|
180
|
+
// How each command actually ran (check-access.ts): the widest is on the receipt and in the log.
|
|
181
|
+
const ranAt = [];
|
|
191
182
|
const bound = (text) => { const hits = scanForSecrets(text); return boundStreamHeadTail(hits.length > 0 ? redactSecretLines(text, hits) : text, 24 * 1024); };
|
|
192
183
|
const note = (label, command, result) => log.push(`=== ${label} ===\n$ ${command}\n(exit ${result.code}${result.notFound ? ", could not start" : ""}${result.timedOut ? ", timed out" : ""})\n\n--- stdout ---\n${bound(result.stdout)}\n\n--- stderr ---\n${bound(result.stderr)}`);
|
|
193
184
|
let result;
|
|
@@ -199,10 +190,11 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
199
190
|
}
|
|
200
191
|
else {
|
|
201
192
|
const setup = store.liveWorktreeSetup(target.repo);
|
|
193
|
+
// Worked out again right before each command (check-access.ts): a narrowing meanwhile applies to the next one.
|
|
202
194
|
let ready = true;
|
|
203
195
|
if (setup !== null) {
|
|
204
196
|
const shell = approvedCommandShell(setup.command);
|
|
205
|
-
const prepared = await runWithIsolatedDatabase(
|
|
197
|
+
const prepared = await runWithIsolatedDatabase(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level)), shell.file, shell.args, { cwd: checkout, timeoutMs: setup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
206
198
|
note("Approved project setup", setup.command, prepared);
|
|
207
199
|
if (prepared.code !== 0 || prepared.timedOut || prepared.notFound) {
|
|
208
200
|
ready = false;
|
|
@@ -217,7 +209,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
217
209
|
}
|
|
218
210
|
else {
|
|
219
211
|
const shell = approvedCommandShell(live.command);
|
|
220
|
-
const ran = await runWithIsolatedDatabase(
|
|
212
|
+
const ran = await runWithIsolatedDatabase(checkoutRunner(store, target.repo, checkout, runner, [runId], level => ranAt.push(level)), shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
221
213
|
note(`${CHECK_LEVEL_WORDS[target.level]} check`, live.command, ran);
|
|
222
214
|
result = ran.timedOut ? { ran: false, failure: "The check ran out of time." } : ran.notFound ? { ran: false, failure: "The check couldn't start." } : { ran: true, exitCode: ran.code };
|
|
223
215
|
}
|
|
@@ -231,10 +223,13 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
231
223
|
rmSync(folder, { recursive: true, force: true });
|
|
232
224
|
}
|
|
233
225
|
const at = now();
|
|
226
|
+
const ranLine = ranAtLine(widestRan(ranAt));
|
|
227
|
+
if (ranLine !== null)
|
|
228
|
+
log.push(ranLine);
|
|
234
229
|
const text = log.length === 0 ? "No command started." : log.join("\n\n");
|
|
235
230
|
const logId = storeEvidence(store, root, runId, "structured-output", `follow-up-check-${request}.log`, Buffer.from(text, "utf8"), FOLLOW_UP_LOG, at, { captureStatus: "ok" });
|
|
236
231
|
const logArtifact = store.getArtifact(logId);
|
|
237
|
-
const receipt = JSON.stringify({ version: 1, request, run: runId, head: target.head, tested: target.head, level: target.level,
|
|
232
|
+
const receipt = JSON.stringify({ version: 1, request, run: runId, head: target.head, tested: target.head, level: target.level, checkAccess: widestRan(ranAt),
|
|
238
233
|
command: { repo: target.command.repo, command: target.command.command, digest: target.command.digest, approvedBy: target.command.approvedBy },
|
|
239
234
|
result, log: { artifactId: logArtifact.id, sha256: logArtifact.sha256, bytesStored: logArtifact.bytesStored, truncated: logArtifact.truncated } }, null, 1);
|
|
240
235
|
storeEvidence(store, root, runId, "structured-output", `follow-up-check-${request}.json`, Buffer.from(receipt, "utf8"), FOLLOW_UP_RECEIPT, at, { captureStatus: "ok" });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux launch helper (sandbox-runtime.ts): `node sandbox-launch.js`, handed one launch in its environment
|
|
3
|
+
* (TOOLROLL_SANDBOX_LAUNCH). It changes into the launch's folder (Anthropic's sandbox-runtime anchors its mandatory
|
|
4
|
+
* write denies there), starts the runtime's network relays to this run's proxy when the network is confined, asks the
|
|
5
|
+
* runtime for the bubblewrap + seccomp command, runs it (setting it up again, from a fresh look, if the sandbox failed
|
|
6
|
+
* before the launch itself started), passes on the signals it gets, and when the command ends removes the mount points
|
|
7
|
+
* the runtime made and stops the relays. It exits with the command's status. Nothing runs if
|
|
8
|
+
* the runtime refuses: the helper exits 126 with the reason.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { checkLinuxDependencies, cleanupBwrapMountPoints, getLinuxDependencyStatus, initializeLinuxNetworkBridge, wrapCommandWithSandboxLinux } from "@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.js";
|
|
12
|
+
import { SANDBOX_LAUNCH_SPEC, shellQuote, withRuntimeTemp } from "./sandbox-runtime.js";
|
|
13
|
+
const REFUSED = 126;
|
|
14
|
+
/** The status a launch stopped before it ran ends with (128 + SIGTERM). */
|
|
15
|
+
const STOPPED = 143;
|
|
16
|
+
/** Why the runtime can't sandbox here now (bubblewrap, socat or its seccomp helper gone), or null: asked again before
|
|
17
|
+
* each attempt, never remembered from an earlier look. */
|
|
18
|
+
function missing() {
|
|
19
|
+
const checked = checkLinuxDependencies();
|
|
20
|
+
if (checked.errors.length > 0)
|
|
21
|
+
return checked.errors.join("; ");
|
|
22
|
+
return getLinuxDependencyStatus().hasSeccompApply ? null : "the sandbox runtime's seccomp helper isn't available";
|
|
23
|
+
}
|
|
24
|
+
async function main() {
|
|
25
|
+
const raw = process.env[SANDBOX_LAUNCH_SPEC];
|
|
26
|
+
delete process.env[SANDBOX_LAUNCH_SPEC];
|
|
27
|
+
if (raw === undefined) {
|
|
28
|
+
process.stderr.write("toolroll sandbox: no launch was given\n");
|
|
29
|
+
return REFUSED;
|
|
30
|
+
}
|
|
31
|
+
const spec = JSON.parse(raw);
|
|
32
|
+
process.chdir(spec.cwd);
|
|
33
|
+
// A stop at any point: what is running is passed it, and nothing starts after it.
|
|
34
|
+
let stopping = false;
|
|
35
|
+
let child = null;
|
|
36
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
|
|
37
|
+
process.on(signal, () => { stopping = true; try {
|
|
38
|
+
child?.kill(signal);
|
|
39
|
+
}
|
|
40
|
+
catch { /* gone */ } });
|
|
41
|
+
const network = spec.terms.network;
|
|
42
|
+
let bridge = null;
|
|
43
|
+
// The relays share the launch's process group: they are waited for (SIGKILL after a moment), so when the helper exits
|
|
44
|
+
// nothing of the launch is left in it to read as the agent's leftover.
|
|
45
|
+
const stopBridge = async () => {
|
|
46
|
+
await Promise.all([bridge?.httpBridgeProcess, bridge?.socksBridgeProcess].map(one => new Promise(done => {
|
|
47
|
+
if (one === undefined || one.exitCode !== null || one.signalCode !== null) {
|
|
48
|
+
done();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const force = setTimeout(() => { try {
|
|
52
|
+
one.kill("SIGKILL");
|
|
53
|
+
}
|
|
54
|
+
catch { /* gone */ } }, 1_000);
|
|
55
|
+
one.once("exit", () => { clearTimeout(force); done(); });
|
|
56
|
+
try {
|
|
57
|
+
one.kill("SIGTERM");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
clearTimeout(force);
|
|
61
|
+
done();
|
|
62
|
+
}
|
|
63
|
+
})));
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
if (network.kind === "proxy")
|
|
67
|
+
bridge = await initializeLinuxNetworkBridge(network.httpPort, network.socksPort ?? network.httpPort);
|
|
68
|
+
// The runtime reads the temp folder it gives the command when it builds it (withRuntimeTemp, sandbox-runtime.ts).
|
|
69
|
+
if (spec.temp !== undefined)
|
|
70
|
+
process.env["CLAUDE_CODE_TMPDIR"] = spec.temp;
|
|
71
|
+
else
|
|
72
|
+
withRuntimeTemp(undefined, () => undefined);
|
|
73
|
+
// The command first says it started, on a descriptor of its own (3), before anything of the launch runs. A sandbox
|
|
74
|
+
// whose setup failed (bubblewrap couldn't mount over a path another launch removed after the runtime looked)
|
|
75
|
+
// never ran the launch, so it is set up again from a fresh look, a few times; one that started is never re-run.
|
|
76
|
+
const started = `printf 1 >&3 || exit 125; exec 3>&-; exec ${shellQuote([spec.file, ...spec.args])}`;
|
|
77
|
+
for (let attempt = 1;; attempt++) {
|
|
78
|
+
// Stopped during setup or between attempts: nothing starts.
|
|
79
|
+
if (stopping)
|
|
80
|
+
return STOPPED;
|
|
81
|
+
const gone = missing();
|
|
82
|
+
if (gone !== null)
|
|
83
|
+
throw new Error(`the sandbox can't be made here: ${gone}`);
|
|
84
|
+
const command = await wrapCommandWithSandboxLinux({
|
|
85
|
+
command: started,
|
|
86
|
+
needsNetworkRestriction: network.kind === "proxy",
|
|
87
|
+
...(bridge === null || network.kind !== "proxy" ? {} : {
|
|
88
|
+
httpSocketPath: bridge.httpSocketPath, socksSocketPath: bridge.socksSocketPath, httpProxyPort: network.httpPort,
|
|
89
|
+
...(network.socksPort === undefined ? {} : { socksProxyPort: network.socksPort }),
|
|
90
|
+
...(network.token === undefined ? {} : { proxyAuthToken: network.token }),
|
|
91
|
+
}),
|
|
92
|
+
readConfig: spec.terms.readConfig,
|
|
93
|
+
writeConfig: spec.terms.writeConfig,
|
|
94
|
+
});
|
|
95
|
+
// bubblewrap, and the runtime's seccomp filter inside it: anything less is not this sandbox.
|
|
96
|
+
if (!/\bbwrap\b/.test(command) || !command.includes("apply-seccomp"))
|
|
97
|
+
throw new Error("the sandbox runtime returned a command without its sandbox or its seccomp filter");
|
|
98
|
+
if (stopping)
|
|
99
|
+
return STOPPED;
|
|
100
|
+
const run = spawn("/bin/sh", ["-c", command], { stdio: ["inherit", "inherit", "pipe", "pipe"] });
|
|
101
|
+
child = run;
|
|
102
|
+
let ran = false;
|
|
103
|
+
const held = [];
|
|
104
|
+
run.stdio[3].on("data", () => { if (!ran) {
|
|
105
|
+
ran = true;
|
|
106
|
+
for (const one of held.splice(0))
|
|
107
|
+
process.stderr.write(one);
|
|
108
|
+
} });
|
|
109
|
+
run.stdio[3].on("error", () => undefined);
|
|
110
|
+
run.stderr.on("data", (chunk) => { if (ran)
|
|
111
|
+
process.stderr.write(chunk);
|
|
112
|
+
else
|
|
113
|
+
held.push(chunk); });
|
|
114
|
+
const status = await new Promise(done => {
|
|
115
|
+
run.on("error", () => done(REFUSED));
|
|
116
|
+
run.on("close", (code, signal) => done(code ?? (signal === null ? 1 : 128 + ({ SIGKILL: 9, SIGTERM: 15, SIGINT: 2, SIGHUP: 1 }[signal] ?? 1))));
|
|
117
|
+
});
|
|
118
|
+
if (!ran && stopping)
|
|
119
|
+
return STOPPED;
|
|
120
|
+
if (ran || attempt >= 3) {
|
|
121
|
+
if (!ran)
|
|
122
|
+
for (const one of held)
|
|
123
|
+
process.stderr.write(one);
|
|
124
|
+
return status;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
cleanupBwrapMountPoints();
|
|
128
|
+
}
|
|
129
|
+
catch { /* the next look is fresh either way */ }
|
|
130
|
+
await new Promise(done => setTimeout(done, 50 * attempt));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (child === null) {
|
|
135
|
+
process.stderr.write(`toolroll sandbox: nothing was started: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
136
|
+
return REFUSED;
|
|
137
|
+
}
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
try {
|
|
142
|
+
cleanupBwrapMountPoints();
|
|
143
|
+
}
|
|
144
|
+
catch { /* best effort: the runtime's own exit handler retries */ }
|
|
145
|
+
await stopBridge();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
void main().then(code => process.exit(code));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FsReadRestrictionConfig, FsWriteRestrictionConfig } from "@anthropic-ai/sandbox-runtime";
|
|
2
|
+
/** The engine and its exact version, as the console and the words say it. */
|
|
3
|
+
export declare const SANDBOX_RUNTIME: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
/** "Anthropic's sandbox-runtime 0.0.79": how every surface names the engine. */
|
|
8
|
+
export declare const sandboxRuntimeWords: () => string;
|
|
9
|
+
/**
|
|
10
|
+
* One launch's sandbox, as the runtime's own config: what it may not read (and the exact paths read again inside
|
|
11
|
+
* those), where it may write (and what stays unwritable there), the extra macOS services it may look up, and its
|
|
12
|
+
* network (open, or only through this run's proxy; a tool broker rides the runtime's second relay).
|
|
13
|
+
*/
|
|
14
|
+
export type SrtTerms = {
|
|
15
|
+
readConfig: FsReadRestrictionConfig;
|
|
16
|
+
writeConfig: FsWriteRestrictionConfig;
|
|
17
|
+
allowMachLookup: string[];
|
|
18
|
+
network: {
|
|
19
|
+
kind: "open";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "proxy";
|
|
22
|
+
httpPort: number;
|
|
23
|
+
socksPort?: number;
|
|
24
|
+
token?: string;
|
|
25
|
+
localServices: boolean;
|
|
26
|
+
unixSockets: string[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/** A deny-by-default profile, as the runtime's are: inside another sandbox one can't be applied (an allow-by-default one
|
|
30
|
+
* still can, so it proves nothing). */
|
|
31
|
+
export declare const SEATBELT_PROBE = "(version 1)(deny default)(allow process-exec)(allow process-fork)(allow file-read*)(allow sysctl-read)(allow mach-lookup)";
|
|
32
|
+
/** Why the runtime can't sandbox here, or null when it can (checked before every launch; nothing runs bare). */
|
|
33
|
+
export declare function sandboxRuntimeProblem(platform?: NodeJS.Platform): string | null;
|
|
34
|
+
/** POSIX shell words. */
|
|
35
|
+
export declare function shellQuote(words: readonly string[]): string;
|
|
36
|
+
/**
|
|
37
|
+
* The runtime sets a sandboxed command's TMPDIR itself (to $CLAUDE_CODE_TMPDIR of the process asking, or /tmp/claude):
|
|
38
|
+
* the launch's own temp folder when it has one, so it keeps it; otherwise /tmp/claude, made if missing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function withRuntimeTemp<T>(temp: string | undefined, make: () => T): T;
|
|
41
|
+
/** The runtime's macOS invocation for `file args` run from `cwd`: a command line for /bin/sh. */
|
|
42
|
+
export declare function srtMacosCommand(file: string, args: readonly string[], terms: SrtTerms, cwd?: string, temp?: string): string;
|
|
43
|
+
/** The Linux helper (sandbox-launch.ts) and the environment variable its launch is handed in. */
|
|
44
|
+
export declare const SANDBOX_LAUNCH_SPEC = "TOOLROLL_SANDBOX_LAUNCH";
|
|
45
|
+
export declare const sandboxLaunchHelper: () => string;
|
|
46
|
+
/** What the Linux helper is given: the command, where it runs, and the terms. */
|
|
47
|
+
export type SandboxLaunchSpec = {
|
|
48
|
+
file: string;
|
|
49
|
+
args: string[];
|
|
50
|
+
cwd: string;
|
|
51
|
+
terms: SrtTerms;
|
|
52
|
+
temp?: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* `file args` inside the runtime's sandbox, as a spawn: on macOS `/bin/sh -c <its command>`; on Linux the helper,
|
|
56
|
+
* handed the launch in its environment (never its arguments, which other users can list).
|
|
57
|
+
*/
|
|
58
|
+
export declare function srtSpawn(file: string, args: readonly string[], terms: SrtTerms, cwd?: string, temp?: string, platform?: NodeJS.Platform): {
|
|
59
|
+
file: string;
|
|
60
|
+
args: string[];
|
|
61
|
+
env: Record<string, string>;
|
|
62
|
+
};
|
|
@@ -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
|
@@ -4,6 +4,7 @@ import { isAcpProfile, type AcpProviderId } from "./acp-agents.js";
|
|
|
4
4
|
export { isAcpProfile };
|
|
5
5
|
import { type NetworkProfile } from "./egress.js";
|
|
6
6
|
import { type WritesMode } from "./writes-mode.js";
|
|
7
|
+
import { type CheckAccess } from "./check-access.js";
|
|
7
8
|
import { type EvidenceKind } from "./contracts/acceptance-terms.js";
|
|
8
9
|
import { type AcceptanceCriterion, type AcceptanceProblem, type ScopeTerms } from "./contracts/scope.js";
|
|
9
10
|
import type { Store, Mutation } from "./store.js";
|
|
@@ -78,6 +79,15 @@ export type ClaudeProfile = {
|
|
|
78
79
|
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
79
80
|
* write allowlists, which keep writing anywhere their fence allows. */
|
|
80
81
|
writes?: WritesMode;
|
|
82
|
+
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
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[];
|
|
81
91
|
};
|
|
82
92
|
export type CodexShapedProfile = {
|
|
83
93
|
provider: "codex" | "openrouter";
|
|
@@ -98,6 +108,15 @@ export type CodexShapedProfile = {
|
|
|
98
108
|
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
99
109
|
* write allowlists, which keep writing anywhere their fence allows. */
|
|
100
110
|
writes?: WritesMode;
|
|
111
|
+
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
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[];
|
|
101
120
|
};
|
|
102
121
|
export type GeminiProfile = {
|
|
103
122
|
provider: "gemini";
|
|
@@ -120,6 +139,15 @@ export type GeminiProfile = {
|
|
|
120
139
|
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
121
140
|
* write allowlists, which keep writing anywhere their fence allows. */
|
|
122
141
|
writes?: WritesMode;
|
|
142
|
+
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
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[];
|
|
123
151
|
};
|
|
124
152
|
/**
|
|
125
153
|
* An agent over the Agent Client Protocol (acp-agents.ts, docs/plans/acp-adapter.md).
|
|
@@ -143,6 +171,15 @@ export type AcpProfile = {
|
|
|
143
171
|
/** Where the build may write (writes-mode.ts), resolved when the scope was written; absent on approvals sealed before
|
|
144
172
|
* write allowlists, which keep writing anywhere their fence allows. */
|
|
145
173
|
writes?: WritesMode;
|
|
174
|
+
/** How far its checks reach beyond the check fence (check-access.ts), sealed when wider than sandboxed; absent: sandboxed. */
|
|
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[];
|
|
146
183
|
};
|
|
147
184
|
export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile | AcpProfile;
|
|
148
185
|
/** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
|