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/agentconfig.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export type ProfileResolution = {
|
|
|
65
65
|
export declare function resolveScopeProfile(store: Store, repo: string | null, ref: Pick<TaskRef, "agentProvider" | "agentModel"> | undefined, flags: PhaseFlags & {
|
|
66
66
|
repairModel?: string | undefined;
|
|
67
67
|
permissionMode?: UnattendedPermissionMode | undefined;
|
|
68
|
-
}): ProfileResolution;
|
|
68
|
+
}, platform?: NodeJS.Platform): ProfileResolution;
|
|
69
69
|
/**
|
|
70
70
|
* The two candidate tiers the routing policy chooses between, per phase,
|
|
71
71
|
* read from configuration ONLY — never inferred from a model's name:
|
package/dist/agentconfig.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { PROJECT_NETWORK_DEFAULT, repositoryEcosystems, resolveNetwork } from "./egress.js";
|
|
23
23
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
24
|
+
import { checkAccessHere } from "./check-access.js";
|
|
25
|
+
import { PROJECT_SANDBOX_DEFAULT } from "./project-sandbox.js";
|
|
24
26
|
import { isProviderId, validateSpec } from "./provider.js";
|
|
25
27
|
import { ACP_LIMITS, CLAUDE_LIMITS, CODEX_SHAPED_LIMITS, GEMINI_LIMITS } from "./scope.js";
|
|
26
28
|
import { ACP_AGENTS, isAcpProvider } from "./acp-agents.js";
|
|
@@ -102,7 +104,7 @@ ref) {
|
|
|
102
104
|
const valid = validateSpec(spec);
|
|
103
105
|
return valid.ok ? { ok: true, spec, source: flags.model !== undefined ? "flag" : source } : { ok: false, problem: valid.problem };
|
|
104
106
|
}
|
|
105
|
-
export function resolveScopeProfile(store, repo, ref, flags) {
|
|
107
|
+
export function resolveScopeProfile(store, repo, ref, flags, platform = process.platform) {
|
|
106
108
|
const build = resolvePhaseAgent(store, "build", repo, flags, ref);
|
|
107
109
|
if (!build.ok)
|
|
108
110
|
return { ok: false, reason: "unknown-provider", problem: build.problem };
|
|
@@ -199,7 +201,14 @@ export function resolveScopeProfile(store, repo, ref, flags) {
|
|
|
199
201
|
const network = resolveNetwork(provider, repo === null ? PROJECT_NETWORK_DEFAULT : store.projectNetwork(repo), repo === null ? [] : repositoryEcosystems(repo));
|
|
200
202
|
// Where the build may write (writes-mode.ts): the project's setting, sealed likewise.
|
|
201
203
|
const writes = repo === null ? WRITES_DEFAULT : store.projectWrites(repo);
|
|
202
|
-
|
|
204
|
+
// How far its checks reach (check-access.ts): the project's setting as checks can get it (sign-ins, off for now,
|
|
205
|
+
// seal as sandboxed: an approval never claims what its checks won't get), sealed only when wider than sandboxed.
|
|
206
|
+
const checkAccess = repo === null ? "sandboxed" : checkAccessHere(store.projectCheckAccess(repo), platform);
|
|
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 } };
|
|
203
212
|
}
|
|
204
213
|
const PHASE_NOUN = { plan: "planner", build: "builder", repair: "repair", review: "reviewer" };
|
|
205
214
|
export function resolveRouteCandidates(store, repo, pins = {}) {
|
package/dist/batch-checks.js
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
* With batching on (a per-project setting, off by default), a Full build doesn't run its check
|
|
4
|
-
* inline. It asks for a batch check on its exact commit, and waits up to the window (10 minutes)
|
|
5
|
-
* for another result of the same project and the same approved command. The batch pass then
|
|
6
|
-
* merges the waiting results onto the recorded project base in a temporary, detached checkout
|
|
7
|
-
* and runs the approved check once:
|
|
8
|
-
*
|
|
9
|
-
* - it passes: every result gets its own sealed receipt, "checked together with" the others,
|
|
10
|
-
* naming the exact batch commit it tested;
|
|
11
|
-
* - it fails: the batch is split in halves and each half checked, down to single results, so the
|
|
12
|
-
* one that breaks it is found and the others still pass on their own;
|
|
13
|
-
* - the results conflict when merged: each is checked on its own.
|
|
14
|
-
*
|
|
15
|
-
* Nothing is merged into a real branch. The batch commit exists only in the temporary checkout,
|
|
16
|
-
* which is always removed; each result still lands on its own.
|
|
17
|
-
*
|
|
18
|
-
* The queue is the follow-up check ledger (result-follow-ups.ts), with `batch` requests: the same
|
|
19
|
-
* claim, receipts, abandoned-check reading, status projection and Merge gate. The policy is an
|
|
20
|
-
* append-only ledger entry, like the check level. */
|
|
21
|
-
import { fencedCheckoutCommands } from "./invoke.js";
|
|
1
|
+
import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
|
|
22
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
23
3
|
import { tmpdir } from "node:os";
|
|
24
4
|
import { join } from "node:path";
|
|
@@ -67,7 +47,8 @@ export async function runBatchChecks(store, root, deps = {}) {
|
|
|
67
47
|
finish("The approved check changed while it waited. Run checks to check it now.");
|
|
68
48
|
continue;
|
|
69
49
|
}
|
|
70
|
-
|
|
50
|
+
// A cohort shares one check level: results whose own checks ran at different levels are checked apart (check-access.ts).
|
|
51
|
+
const key = `${ref.repo}\u0000${one.digest}\u0000${store.runCheckAccess(one.runId)}`;
|
|
71
52
|
cohorts.set(key, [...(cohorts.get(key) ?? []), { request: one.request, runId: one.runId, task: ref.externalId, head: one.head, base: one.base ?? null, at: one.at }]);
|
|
72
53
|
}
|
|
73
54
|
for (const [key, members] of cohorts) {
|
|
@@ -121,6 +102,8 @@ async function checkCohort(store, root, repo, cohort, deps) {
|
|
|
121
102
|
const attempts = new Map(members.map(one => [one.request, []]));
|
|
122
103
|
const bound = (text) => { const hits = scanForSecrets(text); return boundStreamHeadTail(hits.length > 0 ? redactSecretLines(text, hits) : text, 12 * 1024); };
|
|
123
104
|
const section = (label, shown, result) => `=== ${label} ===\n$ ${shown}\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)}`;
|
|
105
|
+
// How each command actually ran (check-access.ts), per result: only the commands it was part of count for its receipt.
|
|
106
|
+
const ranAtOf = new Map();
|
|
124
107
|
const note = (set, text) => { for (const one of set)
|
|
125
108
|
logs.get(one.request).push(text); };
|
|
126
109
|
const attempt = (set, tested, outcome) => { for (const one of set)
|
|
@@ -158,11 +141,15 @@ async function checkCohort(store, root, repo, cohort, deps) {
|
|
|
158
141
|
return { conflict: false, tested, outcome: { ran: false, failure: "Stopped before the check started." }, setupFailed: false };
|
|
159
142
|
// The approvals are re-read before every spawn: a revoked or changed command never runs.
|
|
160
143
|
const liveSetup = store.liveWorktreeSetup(repo);
|
|
144
|
+
// Worked out again right before each command (check-access.ts): a narrowing meanwhile applies to the next one. Each
|
|
145
|
+
// member is credited with exactly the commands of this set.
|
|
146
|
+
const setRan = (level) => { for (const one of set)
|
|
147
|
+
ranAtOf.set(one.request, [...(ranAtOf.get(one.request) ?? []), level]); };
|
|
161
148
|
if ((liveSetup?.digest ?? null) !== (setup?.digest ?? null))
|
|
162
149
|
return { conflict: false, tested, outcome: { ran: false, failure: "The project's approved setup changed before the check started." }, setupFailed: false };
|
|
163
150
|
if (liveSetup !== null) {
|
|
164
151
|
const shell = approvedCommandShell(liveSetup.command);
|
|
165
|
-
const prepared = await runWithIsolatedDatabase(
|
|
152
|
+
const prepared = await runWithIsolatedDatabase(checkoutRunner(store, repo, checkout, runner, set.map(one => one.runId), setRan), shell.file, shell.args, { cwd: checkout, timeoutMs: liveSetup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
166
153
|
note(set, section(`${label} · approved project setup on ${tested.slice(0, 7)}`, liveSetup.command, prepared));
|
|
167
154
|
if (prepared.code !== 0 || prepared.timedOut || prepared.notFound)
|
|
168
155
|
return { conflict: false, tested, outcome: { ran: false, failure: "The project's setup failed, so the check didn't run." }, setupFailed: true };
|
|
@@ -171,7 +158,7 @@ async function checkCohort(store, root, repo, cohort, deps) {
|
|
|
171
158
|
if (live === null || live.digest !== command.digest)
|
|
172
159
|
return { conflict: false, tested, outcome: { ran: false, failure: "The approved check changed before it started." }, setupFailed: false };
|
|
173
160
|
const shell = approvedCommandShell(live.command);
|
|
174
|
-
const ran = await runWithIsolatedDatabase(
|
|
161
|
+
const ran = await runWithIsolatedDatabase(checkoutRunner(store, repo, checkout, runner, set.map(one => one.runId), setRan), shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
175
162
|
note(set, section(`${label} · full check on ${tested.slice(0, 7)}`, live.command, ran));
|
|
176
163
|
return { conflict: false, tested, setupFailed: false,
|
|
177
164
|
outcome: 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 } };
|
|
@@ -183,11 +170,12 @@ async function checkCohort(store, root, repo, cohort, deps) {
|
|
|
183
170
|
};
|
|
184
171
|
const seal = (one, mode, together, tested, outcome) => {
|
|
185
172
|
const at = now();
|
|
186
|
-
const
|
|
173
|
+
const ranLine = ranAtLine(widestRan(ranAtOf.get(one.request) ?? []));
|
|
174
|
+
const text = logs.get(one.request).length === 0 ? "No command started." : [...logs.get(one.request), ...(ranLine === null ? [] : [ranLine])].join("\n\n");
|
|
187
175
|
const logId = storeEvidence(store, root, one.runId, "structured-output", `batch-check-${one.request}.log`, Buffer.from(text, "utf8"), FOLLOW_UP_LOG, at, { captureStatus: "ok" });
|
|
188
176
|
const logArtifact = store.getArtifact(logId);
|
|
189
177
|
const batch = { mode, tested: tested ?? one.head, base, members: together.map(m => ({ task: m.task, run: m.runId, head: m.head })), attempts: attempts.get(one.request) };
|
|
190
|
-
const receipt = JSON.stringify({ version: 1, request: one.request, run: one.runId, head: one.head, tested, level: "full",
|
|
178
|
+
const receipt = JSON.stringify({ version: 1, request: one.request, run: one.runId, head: one.head, tested, level: "full", checkAccess: widestRan(ranAtOf.get(one.request) ?? []),
|
|
191
179
|
command: { repo: command.repo, command: command.command, digest: command.digest, approvedBy: command.approvedBy },
|
|
192
180
|
result: outcome, batch, log: { artifactId: logArtifact.id, sha256: logArtifact.sha256, bytesStored: logArtifact.bytesStored, truncated: logArtifact.truncated } }, null, 1);
|
|
193
181
|
storeEvidence(store, root, one.runId, "structured-output", `batch-check-${one.request}.json`, Buffer.from(receipt, "utf8"), FOLLOW_UP_RECEIPT, at, { captureStatus: "ok" });
|
|
@@ -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.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type NetworkProfile } from "./egress.js";
|
|
2
|
+
import { type CheckAccess } from "./check-access.js";
|
|
1
3
|
import type { WritesMode } from "./writes-mode.js";
|
|
2
4
|
import type { LaunchNetwork } from "./invoke.js";
|
|
3
5
|
import type { ProviderId } from "./contracts/provider.js";
|
|
@@ -187,6 +189,13 @@ export declare function redactSecretAssignments(text: string): string;
|
|
|
187
189
|
/** A build's network (egress.ts): the one its approval sealed, or — sealed before network policies existed — the
|
|
188
190
|
* narrowed legacy one (provider hosts and the repository's registries, nothing added since). */
|
|
189
191
|
export declare function launchNetworkOf(store: Store, taskRef: number, profile: ExecutionProfile): LaunchNetwork;
|
|
192
|
+
/** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
|
|
193
|
+
* run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
|
|
194
|
+
* approval sealed less its agent's own provider hosts (never its checks'). */
|
|
195
|
+
export declare function checkLaunchOf(store: Store, runId: number, profile: ExecutionProfile, platform?: NodeJS.Platform): {
|
|
196
|
+
access: CheckAccess;
|
|
197
|
+
network: NetworkProfile | null;
|
|
198
|
+
};
|
|
190
199
|
/** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
|
|
191
200
|
* its fence allows, as it was approved. */
|
|
192
201
|
export declare function launchWritesOf(profile: ExecutionProfile): WritesMode;
|
package/dist/builder.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { legacyNetwork, repositoryEcosystems } from "./egress.js";
|
|
2
|
+
import { CHECK_ACCESS_DEFAULT, checkAccessHere, checkNetworkOf } from "./check-access.js";
|
|
2
3
|
import { checkoutGit } from "./teardown-git.js";
|
|
3
4
|
import { isAcpProvider } from "./acp-agents.js";
|
|
4
5
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
@@ -50,6 +51,7 @@ import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
|
|
|
50
51
|
import { witnessedRunner } from "./process-custody.js";
|
|
51
52
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
52
53
|
import { recordWorktreeProcess, saveWorkPatch } from "./worktree.js";
|
|
54
|
+
import { launchSandboxOf, protectedChanges, protectedLinks } from "./agent-fence.js";
|
|
53
55
|
import { approvalOf, digestOf, idleClock, isAcpProfile, profileDigestOf, routeParityProblem, watchdogsOf } from "./scope.js";
|
|
54
56
|
import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
|
|
55
57
|
import { execFileSync } from "node:child_process";
|
|
@@ -268,7 +270,26 @@ function buildCheckoutGit(given, worktree, mainCheckout = null) {
|
|
|
268
270
|
/** A run's setup and check commands, in its build's fence, writing where the approval this build runs under sealed
|
|
269
271
|
* (never the scope as it may since have been rewritten; invoke.ts fencedCommands). */
|
|
270
272
|
function commandRunner(store, runId, profile, inner) {
|
|
271
|
-
|
|
273
|
+
// How far its checks reach (check-access.ts): what its approval sealed, stamped on the run (again: the dispatch stamped
|
|
274
|
+
// it first) so a later check of its result never runs wider; under sign-ins, behind its checks' own network.
|
|
275
|
+
const { access, network } = checkLaunchOf(store, runId, profile);
|
|
276
|
+
store.stampRunCheckAccess(runId, access, network);
|
|
277
|
+
return fencedCommands(store, runId, launchWritesOf(profile), inner, undefined, access, network ?? undefined, launchSandboxOf(profile));
|
|
278
|
+
}
|
|
279
|
+
/** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
|
|
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
|
|
281
|
+
* approval sealed less its agent's own provider hosts (never its checks'). */
|
|
282
|
+
export function checkLaunchOf(store, runId, profile, platform = process.platform) {
|
|
283
|
+
const access = checkAccessHere(profile.checkAccess ?? CHECK_ACCESS_DEFAULT, platform);
|
|
284
|
+
if (access !== "sign-ins")
|
|
285
|
+
return { access, network: null };
|
|
286
|
+
const taskRef = store.getRun(runId)?.taskRef;
|
|
287
|
+
const sealed = taskRef === undefined ? undefined : launchNetworkOf(store, taskRef, profile);
|
|
288
|
+
// No sealed network to narrow from: its checks run sandboxed.
|
|
289
|
+
if (sealed === undefined || !("policy" in sealed))
|
|
290
|
+
return { access: CHECK_ACCESS_DEFAULT, network: null };
|
|
291
|
+
const repo = store.refForId(taskRef)?.repo ?? null;
|
|
292
|
+
return { access, network: checkNetworkOf(sealed.policy, profile.provider, repo === null ? { mode: "allowlist", hosts: [], loopback: "proxy-only" } : store.projectNetwork(repo)) };
|
|
272
293
|
}
|
|
273
294
|
/** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
|
|
274
295
|
* its fence allows, as it was approved. */
|
|
@@ -422,6 +443,12 @@ export async function build(store, request) {
|
|
|
422
443
|
// resume below can match on them honestly.
|
|
423
444
|
const provenScopeDigest = scope?.approvedDigest ?? "";
|
|
424
445
|
const provenProfileDigest = profileDigestOf(effective.profile);
|
|
446
|
+
// Its checks' level and network, stamped now with the proof: a batched result that runs no command here still
|
|
447
|
+
// carries what its approval allows its checks (check-access.ts).
|
|
448
|
+
{
|
|
449
|
+
const launch = checkLaunchOf(store, request.runId, effective.profile);
|
|
450
|
+
store.stampRunCheckAccess(request.runId, launch.access, launch.network);
|
|
451
|
+
}
|
|
425
452
|
store.stampRun(request.runId, {
|
|
426
453
|
scopeDigest: provenScopeDigest,
|
|
427
454
|
profileDigest: provenProfileDigest,
|
|
@@ -1350,7 +1377,7 @@ export async function build(store, request) {
|
|
|
1350
1377
|
...watchdogsOf(effective.profile, effective.timeoutMs),
|
|
1351
1378
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
1352
1379
|
network: launchNetworkOf(store, request.taskRef, effective.profile),
|
|
1353
|
-
writes: launchWritesOf(effective.profile),
|
|
1380
|
+
writes: launchWritesOf(effective.profile), sandbox: launchSandboxOf(effective.profile),
|
|
1354
1381
|
...(agent === undefined ? {} : { runner: agent }),
|
|
1355
1382
|
onSpawn: (pid) => { spawned = true; request.onProviderSpawn?.(pid); },
|
|
1356
1383
|
onStreamEvent: (event) => liveLog.observe(event),
|
|
@@ -2694,7 +2721,7 @@ async function resumeUnhandedWork(captured, sessionId) {
|
|
|
2694
2721
|
...watchdogsOf(effective.profile, effective.timeoutMs),
|
|
2695
2722
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
2696
2723
|
network: launchNetworkOf(store, request.taskRef, effective.profile),
|
|
2697
|
-
writes: launchWritesOf(effective.profile),
|
|
2724
|
+
writes: launchWritesOf(effective.profile), sandbox: launchSandboxOf(effective.profile),
|
|
2698
2725
|
...(agent === undefined ? {} : { runner: agent }),
|
|
2699
2726
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
2700
2727
|
clock,
|
|
@@ -2909,7 +2936,7 @@ async function ingestPark(args) {
|
|
|
2909
2936
|
? watchdogsOf(args.profile, args.profile.repairTimeoutSeconds * 1000, true)
|
|
2910
2937
|
: { timeoutMs: args.profile !== undefined ? args.profile.repairTimeoutSeconds * 1000 : REPAIR_TIMEOUT_MS }),
|
|
2911
2938
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
2912
|
-
...(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) }),
|
|
2913
2940
|
...(agent === undefined ? {} : { runner: agent }),
|
|
2914
2941
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
2915
2942
|
clock,
|
|
@@ -3480,6 +3507,25 @@ async function commit(git, worktree, branch, taskId, scope, summary, preparedCan
|
|
|
3480
3507
|
return { ok: true, committed: false, branch, summary };
|
|
3481
3508
|
}
|
|
3482
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
|
+
}
|
|
3483
3529
|
// Setup can create untracked files as well as edit tracked manifests.
|
|
3484
3530
|
// Re-prove the whole staged tree after the normal path exclusions, before
|
|
3485
3531
|
// committing, so neither can widen an exact prepared candidate.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
|
|
3
|
+
* plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
|
|
4
|
+
* of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
|
|
5
|
+
* whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
|
|
6
|
+
* operator's act with a password typed now; any approver for the project may narrow it back.
|
|
7
|
+
*/
|
|
8
|
+
import { type NetworkProfile, type ProjectNetwork } from "./egress.js";
|
|
9
|
+
export declare const CHECK_ACCESS_LEVELS: readonly ["sandboxed", "sign-ins", "unsandboxed"];
|
|
10
|
+
export type CheckAccess = typeof CHECK_ACCESS_LEVELS[number];
|
|
11
|
+
export declare const CHECK_ACCESS_DEFAULT: CheckAccess;
|
|
12
|
+
export declare const isCheckAccess: (value: unknown) => value is CheckAccess;
|
|
13
|
+
/** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
|
|
14
|
+
export declare const checkAccessOf: (value: unknown) => CheckAccess;
|
|
15
|
+
export declare const widerThan: (a: CheckAccess, b: CheckAccess) => boolean;
|
|
16
|
+
/** The narrowest of several levels (none given: the default). */
|
|
17
|
+
export declare const narrowestAccess: (levels: readonly CheckAccess[]) => CheckAccess;
|
|
18
|
+
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
|
+
export declare function checkAccessWords(level: CheckAccess): string;
|
|
20
|
+
/** What each wider level means, said before anyone chooses it. */
|
|
21
|
+
export declare const CHECK_ACCESS_WARNINGS: Readonly<Record<Exclude<CheckAccess, "sandboxed">, string>>;
|
|
22
|
+
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
23
|
+
export declare const SIGN_IN_HOSTS: readonly string[];
|
|
24
|
+
/** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
|
|
25
|
+
* for them: the operator adds them, as a network change of its own. */
|
|
26
|
+
export declare function signInHostsMissing(network: ProjectNetwork): string[];
|
|
27
|
+
/** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
|
|
28
|
+
* do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
|
|
29
|
+
export declare function narrowestNetwork(a: NetworkProfile, b: NetworkProfile): NetworkProfile;
|
|
30
|
+
/** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
|
|
31
|
+
* its checks'); narrower if a project host happens to be one of them. */
|
|
32
|
+
export declare function checkNetworkOf(sealed: NetworkProfile, provider: string, project: ProjectNetwork): NetworkProfile;
|
|
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;
|
|
39
|
+
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
40
|
+
export declare const signInsUnavailableWords: (platform?: NodeJS.Platform) => string | null;
|
|
41
|
+
/** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
|
|
42
|
+
* wider). Everything that seals, launches or stamps a check's level goes through this. */
|
|
43
|
+
export declare const checkAccessHere: (level: CheckAccess, platform?: NodeJS.Platform) => CheckAccess;
|
|
44
|
+
/** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
|
|
45
|
+
* on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
|
|
46
|
+
* sandboxed); otherwise the saved level as this platform can give it. */
|
|
47
|
+
export declare function checkAccessRunsAs(level: CheckAccess, here: {
|
|
48
|
+
platform: NodeJS.Platform;
|
|
49
|
+
fenced: boolean;
|
|
50
|
+
unconfinedAllowed: boolean;
|
|
51
|
+
}): CheckAccess;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
|
|
3
|
+
* plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
|
|
4
|
+
* of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
|
|
5
|
+
* whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
|
|
6
|
+
* operator's act with a password typed now; any approver for the project may narrow it back.
|
|
7
|
+
*/
|
|
8
|
+
import { PROVIDER_HOSTS } from "./egress.js";
|
|
9
|
+
export const CHECK_ACCESS_LEVELS = ["sandboxed", "sign-ins", "unsandboxed"];
|
|
10
|
+
export const CHECK_ACCESS_DEFAULT = "sandboxed";
|
|
11
|
+
export const isCheckAccess = (value) => typeof value === "string" && CHECK_ACCESS_LEVELS.includes(value);
|
|
12
|
+
/** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
|
|
13
|
+
export const checkAccessOf = (value) => isCheckAccess(value) ? value : CHECK_ACCESS_DEFAULT;
|
|
14
|
+
const RANK = { sandboxed: 0, "sign-ins": 1, unsandboxed: 2 };
|
|
15
|
+
export const widerThan = (a, b) => RANK[a] > RANK[b];
|
|
16
|
+
/** The narrowest of several levels (none given: the default). */
|
|
17
|
+
export const narrowestAccess = (levels) => levels.reduce((low, one) => RANK[one] < RANK[low] ? one : low, levels.length === 0 ? CHECK_ACCESS_DEFAULT : "unsandboxed");
|
|
18
|
+
/** The level as a person reads it, in a sentence's middle ("Checks: …"). */
|
|
19
|
+
export function checkAccessWords(level) {
|
|
20
|
+
return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins" : "not sandboxed";
|
|
21
|
+
}
|
|
22
|
+
/** What each wider level means, said before anyone chooses it. */
|
|
23
|
+
export const CHECK_ACCESS_WARNINGS = {
|
|
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. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
|
|
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
|
+
};
|
|
27
|
+
/** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
|
|
28
|
+
export const SIGN_IN_HOSTS = [...PROVIDER_HOSTS["claude"], ...PROVIDER_HOSTS["codex"], "github.com", "api.github.com"];
|
|
29
|
+
/** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
|
|
30
|
+
* for them: the operator adds them, as a network change of its own. */
|
|
31
|
+
export function signInHostsMissing(network) {
|
|
32
|
+
if (network.mode === "open")
|
|
33
|
+
return [];
|
|
34
|
+
const covered = (host) => network.hosts.some(one => one === host || (one.startsWith("*.") && host.endsWith(one.slice(1))));
|
|
35
|
+
return SIGN_IN_HOSTS.filter(host => !covered(host));
|
|
36
|
+
}
|
|
37
|
+
/** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
|
|
38
|
+
* do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
|
|
39
|
+
export function narrowestNetwork(a, b) {
|
|
40
|
+
const loopback = a.loopback === "any" && b.loopback === "any" ? "any" : "proxy-only";
|
|
41
|
+
if (a.mode === "open" && b.mode === "open")
|
|
42
|
+
return { version: 1, mode: "open", hosts: [], loopback };
|
|
43
|
+
if (a.mode === "open")
|
|
44
|
+
return { ...b, loopback };
|
|
45
|
+
if (b.mode === "open")
|
|
46
|
+
return { ...a, loopback };
|
|
47
|
+
const covers = (list, host) => list.some(one => one === host || (one.startsWith("*.") && (host.endsWith(one.slice(1)) || host === one)));
|
|
48
|
+
const hosts = [...new Set([...a.hosts.filter(host => covers(b.hosts, host)), ...b.hosts.filter(host => covers(a.hosts, host))])].sort();
|
|
49
|
+
return { version: 1, mode: "allowlist", hosts, loopback };
|
|
50
|
+
}
|
|
51
|
+
/** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
|
|
52
|
+
* its checks'); narrower if a project host happens to be one of them. */
|
|
53
|
+
export function checkNetworkOf(sealed, provider, project) {
|
|
54
|
+
if (sealed.mode === "open")
|
|
55
|
+
return sealed;
|
|
56
|
+
// A provider host the project itself lists (an operator added it, for its sign-ins, say) is the checks' too.
|
|
57
|
+
const agent = new Set((PROVIDER_HOSTS[provider] ?? []).filter(host => !project.hosts.includes(host)));
|
|
58
|
+
return { ...sealed, hosts: sealed.hosts.filter(host => !agent.has(host)) };
|
|
59
|
+
}
|
|
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;
|
|
66
|
+
/** Why sign-ins can't be chosen here, said plainly (null: they can). */
|
|
67
|
+
export const signInsUnavailableWords = (platform = process.platform) => signInsAvailable(platform) ? null
|
|
68
|
+
: "Not available yet: sign-ins can't be shared with checks safely. Choose Sandboxed or Not sandboxed.";
|
|
69
|
+
/** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
|
|
70
|
+
* wider). Everything that seals, launches or stamps a check's level goes through this. */
|
|
71
|
+
export const checkAccessHere = (level, platform = process.platform) => level === "sign-ins" && !signInsAvailable(platform) ? CHECK_ACCESS_DEFAULT : level;
|
|
72
|
+
/** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
|
|
73
|
+
* on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
|
|
74
|
+
* sandboxed); otherwise the saved level as this platform can give it. */
|
|
75
|
+
export function checkAccessRunsAs(level, here) {
|
|
76
|
+
return !here.fenced && here.unconfinedAllowed ? "unsandboxed" : checkAccessHere(level, here.platform);
|
|
77
|
+
}
|
|
@@ -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.');
|
|
@@ -84,6 +84,8 @@ export declare const CONSOLE_FORMS: {
|
|
|
84
84
|
readonly projectLiveView: FormContract<"act" | "nonce" | "password" | "repo">;
|
|
85
85
|
readonly projectNetwork: FormContract<"hosts" | "local" | "mode" | "password" | "repo">;
|
|
86
86
|
readonly projectWrites: FormContract<"password" | "repo" | "writes">;
|
|
87
|
+
readonly projectCheckAccess: FormContract<"confirm-open" | "level" | "password" | "repo">;
|
|
88
|
+
readonly projectSandbox: FormContract<"mach" | "password" | "read-deny" | "repo">;
|
|
87
89
|
readonly projectDelete: FormContract<"name" | "password" | "repo" | "step">;
|
|
88
90
|
readonly policy: FormContract<"ceiling" | "models" | "password" | "provider" | "tools">;
|
|
89
91
|
readonly approval: FormContract<"not_requester" | "password" | "paths" | "protect" | "repo">;
|
|
@@ -93,6 +93,8 @@ export const CONSOLE_FORMS = {
|
|
|
93
93
|
projectLiveView: formContract("POST /settings/project/live-view", ["repo", "act", "password", "nonce"]),
|
|
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
|
+
projectCheckAccess: formContract("POST /settings/project/check-access", ["repo", "level", "confirm-open", "password"]),
|
|
97
|
+
projectSandbox: formContract("POST /settings/project/sandbox", ["repo", "read-deny", "mach", "password"]),
|
|
96
98
|
projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
|
|
97
99
|
policy: formContract("POST /settings/policy", ["password", "provider", "models", "tools", "ceiling"]),
|
|
98
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
|