toolroll 0.9.69 → 0.9.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +43 -17
- package/THIRD_PARTY_NOTICES.md +21 -1
- package/dist/agent-fence.d.ts +184 -115
- package/dist/agent-fence.js +418 -535
- package/dist/agentconfig.js +6 -1
- package/dist/browser/THIRD_PARTY_NOTICES.txt +21 -0
- package/dist/builder.js +24 -4
- package/dist/check-access.d.ts +6 -9
- package/dist/check-access.js +8 -11
- package/dist/coding-provider.d.ts +14 -0
- package/dist/coding-provider.js +33 -1
- package/dist/coding-workspace.d.ts +15 -0
- package/dist/coding-workspace.js +9 -2
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +4 -0
- package/dist/egress.d.ts +2 -0
- package/dist/egress.js +13 -4
- package/dist/exec.d.ts +17 -3
- package/dist/exec.js +54 -17
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +3 -2
- package/dist/flow-code.d.ts +31 -2
- package/dist/flow-code.js +57 -5
- package/dist/flow-steps.d.ts +4 -0
- package/dist/flow-steps.js +9 -5
- package/dist/flow-triggers.d.ts +4 -0
- package/dist/flow-triggers.js +8 -3
- package/dist/invoke.d.ts +25 -8
- package/dist/invoke.js +115 -115
- package/dist/operate.js +46 -0
- package/dist/project-delete-ui.d.ts +7 -6
- package/dist/project-delete-ui.js +37 -3
- package/dist/project-sandbox.d.ts +59 -0
- package/dist/project-sandbox.js +83 -0
- package/dist/project-tools.d.ts +29 -5
- package/dist/project-tools.js +43 -7
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -0
- package/dist/sandbox-launch.d.ts +1 -0
- package/dist/sandbox-launch.js +148 -0
- package/dist/sandbox-runtime.d.ts +109 -0
- package/dist/sandbox-runtime.js +281 -0
- package/dist/scope.d.ts +28 -0
- package/dist/scope.js +25 -5
- package/dist/scout-browser-lock.d.ts +69 -0
- package/dist/scout-browser-lock.js +69 -0
- package/dist/scout-browser.d.ts +60 -1
- package/dist/scout-browser.js +200 -4
- package/dist/scout.d.ts +9 -4
- package/dist/scout.js +14 -4
- package/dist/serve.js +3 -1
- package/dist/server/render-tasks.js +4 -1
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +27 -0
- package/dist/store.d.ts +31 -1
- package/dist/store.js +83 -4
- package/dist/subagent-tools.d.ts +1 -0
- package/dist/subagent-tools.js +7 -5
- package/dist/tool-broker.d.ts +2 -1
- package/dist/tool-broker.js +2 -2
- package/dist/tool-launcher.js +10 -16
- package/package.json +2 -1
- package/scripts/provider-canary.mjs +5 -6
- package/dist/egress-bridge.d.ts +0 -17
- package/dist/egress-bridge.js +0 -111
package/dist/agentconfig.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { PROJECT_NETWORK_DEFAULT, repositoryEcosystems, resolveNetwork } from "./egress.js";
|
|
23
23
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
24
24
|
import { checkAccessHere } from "./check-access.js";
|
|
25
|
+
import { PROJECT_SANDBOX_DEFAULT } from "./project-sandbox.js";
|
|
25
26
|
import { isProviderId, validateSpec } from "./provider.js";
|
|
26
27
|
import { ACP_LIMITS, CLAUDE_LIMITS, CODEX_SHAPED_LIMITS, GEMINI_LIMITS } from "./scope.js";
|
|
27
28
|
import { ACP_AGENTS, isAcpProvider } from "./acp-agents.js";
|
|
@@ -203,7 +204,11 @@ export function resolveScopeProfile(store, repo, ref, flags, platform = process.
|
|
|
203
204
|
// How far its checks reach (check-access.ts): the project's setting as checks can get it (sign-ins, off for now,
|
|
204
205
|
// seal as sandboxed: an approval never claims what its checks won't get), sealed only when wider than sandboxed.
|
|
205
206
|
const checkAccess = repo === null ? "sandboxed" : checkAccessHere(store.projectCheckAccess(repo), platform);
|
|
206
|
-
|
|
207
|
+
// The fence policy (sandbox parity, docs/plans/sandbox-parity.md §1.5): every new scope is v2; and the project's
|
|
208
|
+
// sandbox settings (project-sandbox.ts), sealed when it lists any.
|
|
209
|
+
const sandbox = repo === null ? PROJECT_SANDBOX_DEFAULT : store.projectSandbox(repo);
|
|
210
|
+
return { ok: true, profile: { ...profile, network, writes, ...(checkAccess === "sandboxed" ? {} : { checkAccess }), fencePolicy: 2,
|
|
211
|
+
...(sandbox.readDeny.length === 0 ? {} : { readDeny: [...sandbox.readDeny] }), ...(sandbox.machLookup.length === 0 ? {} : { machLookup: [...sandbox.machLookup] }) }, provenance: { resolvedFrom: build.source, repairFrom } };
|
|
207
212
|
}
|
|
208
213
|
const PHASE_NOUN = { plan: "planner", build: "builder", repair: "repair", review: "reviewer" };
|
|
209
214
|
export function resolveRouteCandidates(store, repo, pins = {}) {
|
|
@@ -489,6 +489,27 @@ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
489
489
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
490
490
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
491
491
|
|
|
492
|
+
## Anthropic sandbox-runtime — Apache-2.0
|
|
493
|
+
|
|
494
|
+
Copyright Anthropic PBC. https://github.com/anthropics/sandbox-runtime
|
|
495
|
+
|
|
496
|
+
`@anthropic-ai/sandbox-runtime` 0.0.79 is a dependency, installed whole from npm with its own `LICENSE` (and its
|
|
497
|
+
prebuilt Linux seccomp helpers, `vendor/seccomp/<arch>/apply-seccomp`). Toolroll calls it for every sandbox it runs
|
|
498
|
+
(`src/sandbox-runtime.ts`, `src/sandbox-launch.ts`) and copies or modifies none of its code.
|
|
499
|
+
|
|
500
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
501
|
+
you may not use this file except in compliance with the License.
|
|
502
|
+
You may obtain a copy of the License at
|
|
503
|
+
|
|
504
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
505
|
+
|
|
506
|
+
Unless required by applicable law or agreed to in writing, software
|
|
507
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
508
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
509
|
+
See the License for the specific language governing permissions and
|
|
510
|
+
limitations under the License.
|
|
511
|
+
|
|
512
|
+
|
|
492
513
|
## Bundled dependency: @floating-ui/core 1.8.0
|
|
493
514
|
|
|
494
515
|
MIT License
|
package/dist/builder.js
CHANGED
|
@@ -51,6 +51,7 @@ import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
|
|
|
51
51
|
import { witnessedRunner } from "./process-custody.js";
|
|
52
52
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
53
53
|
import { recordWorktreeProcess, saveWorkPatch } from "./worktree.js";
|
|
54
|
+
import { launchSandboxOf, protectedChanges, protectedLinks } from "./agent-fence.js";
|
|
54
55
|
import { approvalOf, digestOf, idleClock, isAcpProfile, profileDigestOf, routeParityProblem, watchdogsOf } from "./scope.js";
|
|
55
56
|
import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
|
|
56
57
|
import { execFileSync } from "node:child_process";
|
|
@@ -273,7 +274,7 @@ function commandRunner(store, runId, profile, inner) {
|
|
|
273
274
|
// it first) so a later check of its result never runs wider; under sign-ins, behind its checks' own network.
|
|
274
275
|
const { access, network } = checkLaunchOf(store, runId, profile);
|
|
275
276
|
store.stampRunCheckAccess(runId, access, network);
|
|
276
|
-
return fencedCommands(store, runId, launchWritesOf(profile), inner, undefined, access, network ?? undefined);
|
|
277
|
+
return fencedCommands(store, runId, launchWritesOf(profile), inner, undefined, access, network ?? undefined, launchSandboxOf(profile));
|
|
277
278
|
}
|
|
278
279
|
/** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
|
|
279
280
|
* run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
|
|
@@ -1376,7 +1377,7 @@ export async function build(store, request) {
|
|
|
1376
1377
|
...watchdogsOf(effective.profile, effective.timeoutMs),
|
|
1377
1378
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
1378
1379
|
network: launchNetworkOf(store, request.taskRef, effective.profile),
|
|
1379
|
-
writes: launchWritesOf(effective.profile),
|
|
1380
|
+
writes: launchWritesOf(effective.profile), sandbox: launchSandboxOf(effective.profile),
|
|
1380
1381
|
...(agent === undefined ? {} : { runner: agent }),
|
|
1381
1382
|
onSpawn: (pid) => { spawned = true; request.onProviderSpawn?.(pid); },
|
|
1382
1383
|
onStreamEvent: (event) => liveLog.observe(event),
|
|
@@ -2720,7 +2721,7 @@ async function resumeUnhandedWork(captured, sessionId) {
|
|
|
2720
2721
|
...watchdogsOf(effective.profile, effective.timeoutMs),
|
|
2721
2722
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
2722
2723
|
network: launchNetworkOf(store, request.taskRef, effective.profile),
|
|
2723
|
-
writes: launchWritesOf(effective.profile),
|
|
2724
|
+
writes: launchWritesOf(effective.profile), sandbox: launchSandboxOf(effective.profile),
|
|
2724
2725
|
...(agent === undefined ? {} : { runner: agent }),
|
|
2725
2726
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
2726
2727
|
clock,
|
|
@@ -2935,7 +2936,7 @@ async function ingestPark(args) {
|
|
|
2935
2936
|
? watchdogsOf(args.profile, args.profile.repairTimeoutSeconds * 1000, true)
|
|
2936
2937
|
: { timeoutMs: args.profile !== undefined ? args.profile.repairTimeoutSeconds * 1000 : REPAIR_TIMEOUT_MS }),
|
|
2937
2938
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
2938
|
-
...(args.profile === undefined ? {} : { network: launchNetworkOf(store, request.taskRef, args.profile), writes: launchWritesOf(args.profile) }),
|
|
2939
|
+
...(args.profile === undefined ? {} : { network: launchNetworkOf(store, request.taskRef, args.profile), writes: launchWritesOf(args.profile), sandbox: launchSandboxOf(args.profile) }),
|
|
2939
2940
|
...(agent === undefined ? {} : { runner: agent }),
|
|
2940
2941
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
2941
2942
|
clock,
|
|
@@ -3506,6 +3507,25 @@ async function commit(git, worktree, branch, taskId, scope, summary, preparedCan
|
|
|
3506
3507
|
return { ok: true, committed: false, branch, summary };
|
|
3507
3508
|
}
|
|
3508
3509
|
}
|
|
3510
|
+
// The protected names' backstop: a result that wrote a hook, Git config, editor or tool settings, a shell profile or
|
|
3511
|
+
// an `.envrc` is refused, however it got written (the sandbox runtime holds the names there when a launch starts; one
|
|
3512
|
+
// made after that, or by Codex, gets through it). The checkout is kept; nothing is committed.
|
|
3513
|
+
const staged = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--name-only", "--no-renames", "-z"], { cwd: worktree });
|
|
3514
|
+
if (staged.code !== 0)
|
|
3515
|
+
return { ok: false, reason: "commit-failure", message: firstLine(staged.stderr) };
|
|
3516
|
+
// And any staged link at a protected name or a folder on the way to one (mode 120000, from the raw listing).
|
|
3517
|
+
const raw = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--raw", "--no-renames", "-z"], { cwd: worktree });
|
|
3518
|
+
if (raw.code !== 0)
|
|
3519
|
+
return { ok: false, reason: "commit-failure", message: firstLine(raw.stderr) };
|
|
3520
|
+
const rawParts = raw.stdout.split("\0");
|
|
3521
|
+
const links = [];
|
|
3522
|
+
for (let at = 0; at + 1 < rawParts.length; at += 2)
|
|
3523
|
+
if (rawParts[at].split(" ")[1] === "120000")
|
|
3524
|
+
links.push(rawParts[at + 1]);
|
|
3525
|
+
const forbidden = [...new Set([...protectedChanges(staged.stdout.split("\0").filter(one => one !== "")), ...protectedLinks(links)])];
|
|
3526
|
+
if (forbidden.length > 0) {
|
|
3527
|
+
return { ok: false, reason: "commit-failure", message: `The result changes ${forbidden.slice(0, 3).join(", ")}${forbidden.length > 3 ? ` and ${forbidden.length - 3} more` : ""}, which builds may not write (they run code or change tools for whoever opens the project next). Nothing was committed; the work is kept in ${worktree}.` };
|
|
3528
|
+
}
|
|
3509
3529
|
// Setup can create untracked files as well as edit tracked manifests.
|
|
3510
3530
|
// Re-prove the whole staged tree after the normal path exclusions, before
|
|
3511
3531
|
// committing, so neither can widen an exact prepared candidate.
|
package/dist/check-access.d.ts
CHANGED
|
@@ -30,15 +30,12 @@ export declare function narrowestNetwork(a: NetworkProfile, b: NetworkProfile):
|
|
|
30
30
|
/** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
|
|
31
31
|
* its checks'); narrower if a project host happens to be one of them. */
|
|
32
32
|
export declare function checkNetworkOf(sealed: NetworkProfile, provider: string, project: ProjectNetwork): NetworkProfile;
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
|
|
40
|
-
export declare const SIGN_INS_READY = false;
|
|
41
|
-
export declare const signInsAvailable: (platform?: NodeJS.Platform) => boolean;
|
|
33
|
+
/** On (0.9.70, sandbox parity C10): a `sign-ins` check reads its sign-in files itself, through its sandbox, by name
|
|
34
|
+
* (agent-fence.ts signInGrants). Nothing privileged reads or copies one, so the race that kept sign-ins off (a folder
|
|
35
|
+
* swapped between a privileged check and its read) has nothing to race; a link planted at a sign-in name leads, inside
|
|
36
|
+
* the sandbox's own view, to a denied path. Every platform with an OS fence. */
|
|
37
|
+
export declare const SIGN_INS_READY = true;
|
|
38
|
+
export declare const signInsAvailable: (_platform?: NodeJS.Platform) => boolean;
|
|
42
39
|
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
43
40
|
export declare const signInsUnavailableWords: (platform?: NodeJS.Platform) => string | null;
|
|
44
41
|
/** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
|
package/dist/check-access.js
CHANGED
|
@@ -17,11 +17,11 @@ export const widerThan = (a, b) => RANK[a] > RANK[b];
|
|
|
17
17
|
export const narrowestAccess = (levels) => levels.reduce((low, one) => RANK[one] < RANK[low] ? one : low, levels.length === 0 ? CHECK_ACCESS_DEFAULT : "unsandboxed");
|
|
18
18
|
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
19
|
export function checkAccessWords(level) {
|
|
20
|
-
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins" : "not sandboxed";
|
|
20
|
+
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins and, on a Mac, Keychain access" : "not sandboxed";
|
|
21
21
|
}
|
|
22
22
|
/** What each wider level means, said before anyone chooses it. */
|
|
23
23
|
export const CHECK_ACCESS_WARNINGS = {
|
|
24
|
-
"sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
|
|
24
|
+
"sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins, even where the project hides your home folder's files from builds. On a Mac that includes Keychain access: they can read any keychain item that lets every program read it, not only those sign-ins. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
|
|
25
25
|
unsandboxed: "This project's checks get your full access to this machine and to Toolroll itself, including its database and every sign-in. Only for a project whose checks must create sandboxes themselves, such as Toolroll's own.",
|
|
26
26
|
};
|
|
27
27
|
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
@@ -57,15 +57,12 @@ export function checkNetworkOf(sealed, provider, project) {
|
|
|
57
57
|
const agent = new Set((PROVIDER_HOSTS[provider] ?? []).filter(host => !project.hosts.includes(host)));
|
|
58
58
|
return { ...sealed, hosts: sealed.hosts.filter(host => !agent.has(host)) };
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
|
|
67
|
-
export const SIGN_INS_READY = false;
|
|
68
|
-
export const signInsAvailable = (platform = process.platform) => SIGN_INS_READY && platform !== "linux";
|
|
60
|
+
/** On (0.9.70, sandbox parity C10): a `sign-ins` check reads its sign-in files itself, through its sandbox, by name
|
|
61
|
+
* (agent-fence.ts signInGrants). Nothing privileged reads or copies one, so the race that kept sign-ins off (a folder
|
|
62
|
+
* swapped between a privileged check and its read) has nothing to race; a link planted at a sign-in name leads, inside
|
|
63
|
+
* the sandbox's own view, to a denied path. Every platform with an OS fence. */
|
|
64
|
+
export const SIGN_INS_READY = true;
|
|
65
|
+
export const signInsAvailable = (_platform = process.platform) => SIGN_INS_READY;
|
|
69
66
|
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
70
67
|
export const signInsUnavailableWords = (platform = process.platform) => signInsAvailable(platform) ? null
|
|
71
68
|
: "Not available yet: sign-ins can't be shared with checks safely. Choose Sandboxed or Not sandboxed.";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CodingPermissions } from "./coding-workspace.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
1
3
|
import { type ProcessObservationFailure } from "./process-tree.js";
|
|
2
4
|
/** Read-only recovery witnesses, never authority to signal historical PIDs.
|
|
3
5
|
* macOS ancestry is observational; a missed/failed observation stays fenced.
|
|
@@ -70,3 +72,15 @@ export declare function createCodexCodingProvider(options?: {
|
|
|
70
72
|
command?: string;
|
|
71
73
|
args?: string[];
|
|
72
74
|
}): CodingProvider;
|
|
75
|
+
/**
|
|
76
|
+
* An interactive Codex session's confinement (docs/plans/sandbox-parity.md C3): the permission profile a Codex build
|
|
77
|
+
* gets (agent-fence.ts codexFenceConfig), as the thread's config overrides: Toolroll's state, every other launch's
|
|
78
|
+
* folder and every other checkout in the plane's pools and the coding sessions' own pool denied, the session's own
|
|
79
|
+
* worktree given back; Codex's workspace writes; no network (Codex's command sandbox can't route through a proxy, and
|
|
80
|
+
* sessions have never had one). The worktree is bound to its project by the plane's own records first; anything that
|
|
81
|
+
* doesn't prove refuses the session.
|
|
82
|
+
*/
|
|
83
|
+
export declare function codingSessionPermissions(store: Pick<Store, "databaseFile" | "listWorktrees" | "projectSandbox">, worktreeRoot: string): (input: {
|
|
84
|
+
repo: string;
|
|
85
|
+
worktree: string;
|
|
86
|
+
}) => CodingPermissions;
|
package/dist/coding-provider.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { TextDecoder } from "node:util";
|
|
3
|
-
import { hostname } from "node:os";
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { readDenyPath } from "./project-sandbox.js";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { codexFenceConfig, fenceFor, FENCE_POLICY_CURRENT, runFolders, taskBoundary, trustedWorktreeBinding } from "./agent-fence.js";
|
|
7
|
+
import { poolRootOf } from "./worktree.js";
|
|
4
8
|
import { currentBootId } from "./boot-identity.js";
|
|
5
9
|
import { createContainer, currentContainment } from "./containment.js";
|
|
6
10
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
@@ -423,3 +427,31 @@ export function createCodexCodingProvider(options = {}) {
|
|
|
423
427
|
close,
|
|
424
428
|
};
|
|
425
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* An interactive Codex session's confinement (docs/plans/sandbox-parity.md C3): the permission profile a Codex build
|
|
432
|
+
* gets (agent-fence.ts codexFenceConfig), as the thread's config overrides: Toolroll's state, every other launch's
|
|
433
|
+
* folder and every other checkout in the plane's pools and the coding sessions' own pool denied, the session's own
|
|
434
|
+
* worktree given back; Codex's workspace writes; no network (Codex's command sandbox can't route through a proxy, and
|
|
435
|
+
* sessions have never had one). The worktree is bound to its project by the plane's own records first; anything that
|
|
436
|
+
* doesn't prove refuses the session.
|
|
437
|
+
*/
|
|
438
|
+
export function codingSessionPermissions(store, worktreeRoot) {
|
|
439
|
+
return input => sessionPermissions(store, { ...input, worktreeRoot });
|
|
440
|
+
}
|
|
441
|
+
function sessionPermissions(store, input) {
|
|
442
|
+
const databaseFile = store.databaseFile();
|
|
443
|
+
if (databaseFile === null || databaseFile === "" || databaseFile === ":memory:")
|
|
444
|
+
return { problem: "The coding session has no state folder to fence, so it was not started." };
|
|
445
|
+
try {
|
|
446
|
+
const poolRoots = [join(dirname(databaseFile), "worktrees"), input.worktreeRoot, ...store.listWorktrees().map(record => poolRootOf(record.path, record.repo))];
|
|
447
|
+
const binding = trustedWorktreeBinding(input.repo, input.worktree, poolRoots);
|
|
448
|
+
const boundary = taskBoundary({ worktree: input.worktree, provider: "codex", binding, policy: FENCE_POLICY_CURRENT });
|
|
449
|
+
// The project's read denies, read live (a session seals none; they only narrow).
|
|
450
|
+
const readDeny = store.projectSandbox(input.repo).readDeny.map(one => readDenyPath(one, homedir()));
|
|
451
|
+
const launch = fenceFor("build", { databaseFile, worktree: input.worktree, boundary, binding, runFolders: runFolders(null), poolRoots, readDeny, policy: FENCE_POLICY_CURRENT });
|
|
452
|
+
return { config: codexFenceConfig(launch.fence, false, launch.fenceGrants) };
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return { problem: "The session's worktree couldn't be bound to its project, so the session was not started." };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -24,9 +24,22 @@ export declare class CodingActionError extends Error {
|
|
|
24
24
|
export declare class CodingStateConflictError extends CodingActionError {
|
|
25
25
|
constructor(id: string);
|
|
26
26
|
}
|
|
27
|
+
/** How a session's commands are confined (coding-provider.ts codingSessionPermissions): Toolroll's permission profile as
|
|
28
|
+
* the thread's config overrides, or a refusal. Required: no session starts without its caller saying which. */
|
|
29
|
+
export type CodingPermissions = {
|
|
30
|
+
config: Record<string, unknown>;
|
|
31
|
+
} | {
|
|
32
|
+
sandbox: 'workspace-write';
|
|
33
|
+
} | {
|
|
34
|
+
problem: string;
|
|
35
|
+
};
|
|
27
36
|
type Options = {
|
|
28
37
|
database: string;
|
|
29
38
|
worktreeRoot: string;
|
|
39
|
+
permissions: (input: {
|
|
40
|
+
repo: string;
|
|
41
|
+
worktree: string;
|
|
42
|
+
}) => CodingPermissions;
|
|
30
43
|
provider?: () => CodingProvider;
|
|
31
44
|
admissionPaused?: () => boolean;
|
|
32
45
|
authorize?: (actor: CodingActor, repo: string) => boolean;
|
|
@@ -92,6 +105,8 @@ export declare class CodingWorkspace {
|
|
|
92
105
|
private putItem;
|
|
93
106
|
private flush;
|
|
94
107
|
private item;
|
|
108
|
+
/** The thread's confinement, made right before each start or resume from the plane's own records. */
|
|
109
|
+
private permissionsFor;
|
|
95
110
|
private native;
|
|
96
111
|
private request;
|
|
97
112
|
private event;
|
package/dist/coding-workspace.js
CHANGED
|
@@ -278,6 +278,13 @@ export class CodingWorkspace {
|
|
|
278
278
|
const clientId = string(item['clientId']) || string(item['clientUserMessageId']);
|
|
279
279
|
return { id, type, text, status: string(item['status']) || null, ...(type === 'userMessage' && clientId ? { clientId } : {}) };
|
|
280
280
|
}
|
|
281
|
+
/** The thread's confinement, made right before each start or resume from the plane's own records. */
|
|
282
|
+
permissionsFor(repo, worktree) {
|
|
283
|
+
const made = this.options.permissions({ repo, worktree });
|
|
284
|
+
if ('problem' in made)
|
|
285
|
+
throw Error(made.problem);
|
|
286
|
+
return made;
|
|
287
|
+
}
|
|
281
288
|
native() {
|
|
282
289
|
if (this.closed || this.closing || this.persistenceFailed)
|
|
283
290
|
throw Error('The coding server cannot save activity. Reopen the workspace after the server recovers.');
|
|
@@ -445,7 +452,7 @@ export class CodingWorkspace {
|
|
|
445
452
|
this.authorize(actor, repo);
|
|
446
453
|
this.admit();
|
|
447
454
|
nativeStartupAttempted = true;
|
|
448
|
-
const result = object(await this.request('thread/start', { cwd: worktree, approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
455
|
+
const result = object(await this.request('thread/start', { cwd: worktree, approvalPolicy: 'on-request', approvalsReviewer: 'user', ...this.permissionsFor(repo, worktree), ...(input.model ? { model: input.model } : {}), developerInstructions: CODING_INSTRUCTIONS + (context?.text ?? ''), persistExtendedHistory: true }));
|
|
449
456
|
const thread = object(result['thread']), nativeThreadId = string(thread['id']);
|
|
450
457
|
if (!nativeThreadId)
|
|
451
458
|
throw Error('The agent did not return a session identity.');
|
|
@@ -494,7 +501,7 @@ export class CodingWorkspace {
|
|
|
494
501
|
this.authorize({ name: session.owner, generation: session.generation }, session.repo);
|
|
495
502
|
if (session.context)
|
|
496
503
|
verifyCodingContext(session.context, session.repo, session.base);
|
|
497
|
-
const result = object(await this.request('thread/resume', { threadId: session.nativeThreadId, cwd: session.worktree, approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
504
|
+
const result = object(await this.request('thread/resume', { threadId: session.nativeThreadId, cwd: session.worktree, approvalPolicy: 'on-request', approvalsReviewer: 'user', ...this.permissionsFor(session.repo, session.worktree), ...(session.model ? { model: session.model } : {}), developerInstructions: CODING_INSTRUCTIONS + (session.context?.text ?? ''), persistExtendedHistory: true }));
|
|
498
505
|
const thread = object(result['thread']);
|
|
499
506
|
if (thread['id'] !== session.nativeThreadId)
|
|
500
507
|
throw Error('The agent returned a different conversation. Continuity could not be verified.');
|
|
@@ -85,6 +85,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
85
85
|
readonly projectNetwork: FormContract<"hosts" | "local" | "mode" | "password" | "repo">;
|
|
86
86
|
readonly projectWrites: FormContract<"password" | "repo" | "writes">;
|
|
87
87
|
readonly projectCheckAccess: FormContract<"confirm-open" | "level" | "password" | "repo">;
|
|
88
|
+
readonly projectSandbox: FormContract<"mach" | "password" | "read-deny" | "repo">;
|
|
88
89
|
readonly projectDelete: FormContract<"name" | "password" | "repo" | "step">;
|
|
89
90
|
readonly policy: FormContract<"ceiling" | "models" | "password" | "provider" | "tools">;
|
|
90
91
|
readonly approval: FormContract<"not_requester" | "password" | "paths" | "protect" | "repo">;
|
|
@@ -94,6 +94,7 @@ export const CONSOLE_FORMS = {
|
|
|
94
94
|
projectNetwork: formContract("POST /settings/project/network", ["repo", "mode", "local", "hosts", "password"]),
|
|
95
95
|
projectWrites: formContract("POST /settings/project/writes", ["repo", "writes", "password"]),
|
|
96
96
|
projectCheckAccess: formContract("POST /settings/project/check-access", ["repo", "level", "confirm-open", "password"]),
|
|
97
|
+
projectSandbox: formContract("POST /settings/project/sandbox", ["repo", "read-deny", "mach", "password"]),
|
|
97
98
|
projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
|
|
98
99
|
policy: formContract("POST /settings/policy", ["password", "provider", "models", "tools", "ceiling"]),
|
|
99
100
|
approval: formContract("POST /settings/approval", ["repo", "password", "protect", "paths", "not_requester"]),
|
package/dist/doctor.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export type DoctorReport = {
|
|
|
17
17
|
findings: DoctorFinding[];
|
|
18
18
|
unreadable: DoctorUnreadable[];
|
|
19
19
|
notes: string[];
|
|
20
|
+
/** The sandbox every build runs in (sandbox-runtime.ts): Anthropic's sandbox-runtime, its version, and why it can't
|
|
21
|
+
* sandbox here (null when it can). */
|
|
22
|
+
sandbox?: {
|
|
23
|
+
engine: string;
|
|
24
|
+
version: string;
|
|
25
|
+
problem: string | null;
|
|
26
|
+
};
|
|
20
27
|
};
|
|
21
28
|
export type DoctorDeps = {
|
|
22
29
|
platform?: NodeJS.Platform;
|
package/dist/doctor.js
CHANGED
|
@@ -24,6 +24,7 @@ import { envelopeJson } from "./envelope.js";
|
|
|
24
24
|
import { run as runCommand } from "./exec.js";
|
|
25
25
|
import { OWN_KEY_ENV } from "./keys.js";
|
|
26
26
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
27
|
+
import { SANDBOX_RUNTIME, sandboxRuntimeProblem, sandboxRuntimeWords } from "./sandbox-runtime.js";
|
|
27
28
|
/** Names that say they hold a credential. */
|
|
28
29
|
const CREDENTIAL_NAME = /(?:^|_)(?:API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|SECRET|SECRET_?KEY|TOKEN|AUTH_?TOKEN|PASSWORD|PASSWD|CREDENTIALS?|PAT)(?:_|$)/i;
|
|
29
30
|
/** Names that only point at one: a path, a socket, an address. */
|
|
@@ -401,6 +402,8 @@ export function doctorLines(report) {
|
|
|
401
402
|
}
|
|
402
403
|
for (const note of report.notes)
|
|
403
404
|
lines.push("", `Note: ${note}`);
|
|
405
|
+
if (report.sandbox !== undefined)
|
|
406
|
+
lines.push("", `Sandbox: ${sandboxRuntimeWords()}${report.sandbox.problem === null ? ", working here." : `, not available here: ${report.sandbox.problem}. Builds don't start unless an instance operator allows unconfined agents.`}`);
|
|
404
407
|
if (report.checked.length > 0)
|
|
405
408
|
lines.push("", "Checked:", ...report.checked.map(one => ` ${one}`));
|
|
406
409
|
return lines;
|
|
@@ -418,6 +421,7 @@ export async function runDoctorCommand(argv, write, deps = {}) {
|
|
|
418
421
|
return 2;
|
|
419
422
|
}
|
|
420
423
|
const report = await auditServiceEnvironment(deps);
|
|
424
|
+
report.sandbox = { engine: SANDBOX_RUNTIME.name, version: SANDBOX_RUNTIME.version, problem: sandboxRuntimeProblem() };
|
|
421
425
|
if (json)
|
|
422
426
|
write(envelopeJson({ command: "doctor", ...report }));
|
|
423
427
|
else
|
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;
|