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/operate.js
CHANGED
|
@@ -112,6 +112,9 @@ import { claimActor, currentActor, withActor } from "./actor.js";
|
|
|
112
112
|
import { parseLeadToken } from "./api-tokens.js";
|
|
113
113
|
import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
|
|
114
114
|
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
115
|
+
import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
|
|
116
|
+
import { parseMachLookup, parseReadDeny, readDenyWords } from "./project-sandbox.js";
|
|
117
|
+
import { CHECK_ACCESS_WARNINGS, checkAccessRunsAs, checkAccessWords, isCheckAccess, signInHostsMissing, signInsUnavailableWords, widerThan } from "./check-access.js";
|
|
115
118
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
116
119
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
117
120
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -265,6 +268,9 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
265
268
|
where the project's builds may write: only their checkout, caches and temp
|
|
266
269
|
(workspace, the default), or anywhere outside Toolroll's fenced folders;
|
|
267
270
|
an approver changes it (new scopes seal it; approved ones keep theirs)
|
|
271
|
+
toolroll project check-access --repo <p> [sandboxed|sign-ins|unsandboxed]
|
|
272
|
+
how far the project's checks reach: sandboxed (the default), with model
|
|
273
|
+
and GitHub sign-ins, or not sandboxed; an instance operator widens it
|
|
268
274
|
toolroll config unconfined-agents [allow|refuse]
|
|
269
275
|
whether builds run where this machine can't confine them (instance operator)
|
|
270
276
|
toolroll project concurrency [<n>] --repo <p>
|
|
@@ -686,6 +692,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
686
692
|
"token-name",
|
|
687
693
|
// task pause / task steer --now: the asker's own request id, so a repeat is the same request.
|
|
688
694
|
"request",
|
|
695
|
+
// project sandbox (sandbox parity C9): read denies and macOS services.
|
|
696
|
+
"read-deny", "mach",
|
|
689
697
|
]);
|
|
690
698
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
691
699
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
|
|
@@ -916,6 +924,8 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
|
|
|
916
924
|
...(options.flowTriggerIo === undefined ? {} : { flowTriggerIo: options.flowTriggerIo }),
|
|
917
925
|
...(options.flowStepIo === undefined ? {} : { flowStepIo: options.flowStepIo }),
|
|
918
926
|
...(options.flowEveryMs === undefined ? {} : { flowEveryMs: options.flowEveryMs }),
|
|
927
|
+
...(options.platform === undefined ? {} : { platform: options.platform }),
|
|
928
|
+
...(options.fenceAvailable === undefined ? {} : { fenceAvailable: options.fenceAvailable }),
|
|
919
929
|
...(options.flowFetch === undefined ? {} : { flowFetch: options.flowFetch }),
|
|
920
930
|
...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
|
|
921
931
|
...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
|
|
@@ -1171,6 +1181,10 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1171
1181
|
return projectNetworkCommand(positional, flags, context);
|
|
1172
1182
|
if (positional[0] === "writes")
|
|
1173
1183
|
return projectWritesCommand(positional, flags, context);
|
|
1184
|
+
if (positional[0] === "check-access")
|
|
1185
|
+
return projectCheckAccessCommand(positional, flags, context);
|
|
1186
|
+
if (positional[0] === "sandbox")
|
|
1187
|
+
return projectSandboxCommand(positional, flags, context);
|
|
1174
1188
|
if (positional[0] === "checks")
|
|
1175
1189
|
return projectChecksCommand(positional, flags, context);
|
|
1176
1190
|
if (positional[0] === "demo")
|
|
@@ -11567,3 +11581,108 @@ export function ownValueFlags(command, argv) {
|
|
|
11567
11581
|
return PROJECT_NETWORK_VALUE_FLAGS;
|
|
11568
11582
|
return undefined;
|
|
11569
11583
|
}
|
|
11584
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11585
|
+
/**
|
|
11586
|
+
* `project check-access --repo <p> [sandboxed|sign-ins|unsandboxed]` (check-access.ts): how far the project's setup
|
|
11587
|
+
* and check commands reach beyond the check fence. Widening is an instance operator's act with a password typed now
|
|
11588
|
+
* (never a remembered sign-in or a single sign-on recheck); any approver for the project may narrow it back to
|
|
11589
|
+
* sandboxed. New scopes seal it; approved ones keep theirs. The ledger keeps before → after.
|
|
11590
|
+
*/
|
|
11591
|
+
async function projectCheckAccessCommand(positional, flags, context) {
|
|
11592
|
+
const command = "project check-access";
|
|
11593
|
+
const allowed = new Set(["repo", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
11594
|
+
for (const name of flags.keys())
|
|
11595
|
+
if (!allowed.has(name))
|
|
11596
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project check-access option.`, EXIT.usage);
|
|
11597
|
+
const repoFlag = text(flags, "repo");
|
|
11598
|
+
const wanted = positional[1];
|
|
11599
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isCheckAccess(wanted)))
|
|
11600
|
+
return fail(context.write, context.json, command, "usage", "Use project check-access --repo <project path> [sandboxed | sign-ins | unsandboxed].", EXIT.usage);
|
|
11601
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11602
|
+
if (repo === undefined)
|
|
11603
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11604
|
+
const current = context.store.projectCheckAccess(repo);
|
|
11605
|
+
const platform = context.platform ?? process.platform;
|
|
11606
|
+
// How it actually runs here: with no OS fence and unconfined running allowed, without a sandbox whatever is saved; on
|
|
11607
|
+
// a saved sign-ins level (off for now) runs sandboxed. Said, never hidden.
|
|
11608
|
+
const fenced = context.fenceAvailable?.() ?? (macosFenceAvailable() || linuxFenceAvailable());
|
|
11609
|
+
const runsAs = (one) => checkAccessRunsAs(one, { platform, fenced, unconfinedAllowed: context.store.unfencedAgentsAllowed() });
|
|
11610
|
+
const here = runsAs(current);
|
|
11611
|
+
const words = (one) => {
|
|
11612
|
+
if (runsAs(one) === "unsandboxed" && one !== "unsandboxed")
|
|
11613
|
+
return [`${repo}: saved as ${checkAccessWords(one)}, but this machine can't sandbox checks, so they run without a sandbox.`];
|
|
11614
|
+
if (runsAs(one) !== one)
|
|
11615
|
+
return [`${repo}: saved with model and GitHub sign-ins, but its checks run sandboxed: sign-ins can't be shared with checks safely yet.`];
|
|
11616
|
+
const missing = one === "sign-ins" ? signInHostsMissing(context.store.projectNetwork(repo)) : [];
|
|
11617
|
+
return [`${repo}: checks run ${checkAccessWords(one)}.`, ...(one === "sandboxed" ? [] : [CHECK_ACCESS_WARNINGS[one]]),
|
|
11618
|
+
// Never added for it: the operator widens the network separately, with their own password.
|
|
11619
|
+
...(missing.length === 0 ? [] : [`Model sign-ins need these hosts in this project's network allowlist: ${missing.join(", ")}. Add them with: toolroll project network --repo ${repo} --allow ${missing.join(",")}`])];
|
|
11620
|
+
};
|
|
11621
|
+
if (wanted === undefined)
|
|
11622
|
+
return succeed(context.write, context.json, command, { repo, checkAccess: current, ...(here === current ? {} : { runsAs: here }), ...(here === "sign-ins" ? { missingHosts: signInHostsMissing(context.store.projectNetwork(repo)) } : {}) }, () => words(current));
|
|
11623
|
+
// Not available yet (check-access.ts signInsAvailable): refused before any password is asked for, nothing written.
|
|
11624
|
+
const unavailable = wanted === "sign-ins" ? signInsUnavailableWords(platform) : null;
|
|
11625
|
+
if (unavailable !== null)
|
|
11626
|
+
return fail(context.write, context.json, command, "refused", unavailable, EXIT.refused);
|
|
11627
|
+
const widening = widerThan(wanted, current);
|
|
11628
|
+
const acting = await askCredentials(flags, context);
|
|
11629
|
+
const proved = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token, repo);
|
|
11630
|
+
if (widening) {
|
|
11631
|
+
// Wider reach exposes the operator's own sign-ins or machine: an instance operator, with a password typed for this.
|
|
11632
|
+
if (acting === null || proved === null || !proved.ok || proved.method !== "password-now" || !context.store.isInstanceOperator(acting.name)) {
|
|
11633
|
+
return fail(context.write, context.json, command, "refused", "An instance operator widens what a project's checks reach, with their password typed now: pass --as and --token.", EXIT.refused);
|
|
11634
|
+
}
|
|
11635
|
+
}
|
|
11636
|
+
else if (acting === null || proved === null || !proved.ok || !credentialProof(proved.method) || !context.store.accountCanAccess(acting.name, repo)) {
|
|
11637
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project narrows what its checks reach, with their password: pass --as and --token.", EXIT.refused);
|
|
11638
|
+
}
|
|
11639
|
+
// The level is compared again inside the write: a change made while the password was asked for can't widen it here.
|
|
11640
|
+
const changed = context.store.setProjectCheckAccess(repo, wanted, acting.name, context.clock(), { may: widening ? "widen" : "narrow" });
|
|
11641
|
+
if (changed === null)
|
|
11642
|
+
return fail(context.write, context.json, command, "refused", "The project's check access changed meanwhile, and this would now widen it: an instance operator widens it, with their password typed now.", EXIT.refused);
|
|
11643
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, checkAccess: changed.after }, () => [
|
|
11644
|
+
...words(changed.after),
|
|
11645
|
+
"Scopes written from now on use this; scopes already approved keep what they were approved with.",
|
|
11646
|
+
]);
|
|
11647
|
+
}
|
|
11648
|
+
/**
|
|
11649
|
+
* `project sandbox --repo <p> [--read-deny <a,b>] [--mach <a,b>]` (project-sandbox.ts, sandbox parity C9): what the
|
|
11650
|
+
* project's builds and checks may not read (paths, `~/…` or absolute) and the macOS services they may also use. Each list is replaced whole by its flag (an empty value clears it). New scopes seal it;
|
|
11651
|
+
* approved ones keep theirs. Changing it is an approver's act with a password, kept in the ledger as before → after.
|
|
11652
|
+
*/
|
|
11653
|
+
async function projectSandboxCommand(positional, flags, context) {
|
|
11654
|
+
const command = "project sandbox";
|
|
11655
|
+
const allowed = new Set(["repo", "read-deny", "mach", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
11656
|
+
for (const name of flags.keys())
|
|
11657
|
+
if (!allowed.has(name))
|
|
11658
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project sandbox option.`, EXIT.usage);
|
|
11659
|
+
const repoFlag = text(flags, "repo");
|
|
11660
|
+
if (positional.length > 1 || repoFlag === undefined)
|
|
11661
|
+
return fail(context.write, context.json, command, "usage", "Use project sandbox --repo <project path> [--read-deny <paths>] [--mach <services>] (comma-separated; empty clears).", EXIT.usage);
|
|
11662
|
+
const repo = await knownProject(context, repoFlag, { listed: true, canonical: true });
|
|
11663
|
+
if (repo === undefined)
|
|
11664
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11665
|
+
const words = (one) => [`${repo}: ${readDenyWords(one.readDeny)}`,
|
|
11666
|
+
...(one.machLookup.length === 0 ? [] : [`macOS services also allowed: ${one.machLookup.join(", ")}`])];
|
|
11667
|
+
const current = context.store.projectSandbox(repo);
|
|
11668
|
+
const asked = (name) => { const value = flags.get(name); return value === undefined ? undefined : value === true ? [] : value.split(","); };
|
|
11669
|
+
const changes = { readDeny: asked("read-deny"), machLookup: asked("mach") };
|
|
11670
|
+
if (changes.readDeny === undefined && changes.machLookup === undefined)
|
|
11671
|
+
return succeed(context.write, context.json, command, { repo, sandbox: current }, () => words(current));
|
|
11672
|
+
const parsed = { readDeny: parseReadDeny(changes.readDeny ?? current.readDeny), machLookup: parseMachLookup(changes.machLookup ?? current.machLookup) };
|
|
11673
|
+
for (const [name, one] of Object.entries(parsed))
|
|
11674
|
+
if (!one.ok)
|
|
11675
|
+
return fail(context.write, context.json, command, "usage", `${name}: ${one.problem}. Nothing changed.`, EXIT.usage);
|
|
11676
|
+
const wanted = { readDeny: parsed.readDeny.ok ? parsed.readDeny.list : [], machLookup: parsed.machLookup.ok ? parsed.machLookup.list : [] };
|
|
11677
|
+
const acting = await askCredentials(flags, context);
|
|
11678
|
+
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
11679
|
+
if (acting === null || !verified?.ok || !context.store.accountCanAccess(acting.name, repo))
|
|
11680
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project changes its sandbox, with their password: pass --as and --token.", EXIT.refused);
|
|
11681
|
+
const changed = context.store.setProjectSandbox(repo, wanted, acting.name, context.clock());
|
|
11682
|
+
if (changed === null)
|
|
11683
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project changes its sandbox. Nothing changed.", EXIT.refused);
|
|
11684
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, sandbox: changed.after }, () => [
|
|
11685
|
+
...words(changed.after),
|
|
11686
|
+
"Scopes written from now on use this; scopes already approved keep what they were approved with.",
|
|
11687
|
+
]);
|
|
11688
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* Settings → Project: what Toolroll holds for a project, and deleting
|
|
3
|
-
* it. An instance operator deletes in two steps: type the project's name,
|
|
4
|
-
* then read exactly what goes and confirm with their password (or a fresh
|
|
5
|
-
* sign-in with the identity provider). The repository itself stays.
|
|
6
|
-
*/
|
|
1
|
+
import type { ProjectSandbox } from "./project-sandbox.js";
|
|
7
2
|
import type { ProjectNetwork } from "./egress.js";
|
|
8
3
|
import type { WritesMode } from "./writes-mode.js";
|
|
4
|
+
import { type CheckAccess } from "./check-access.js";
|
|
9
5
|
import { type Html } from "./html.js";
|
|
10
6
|
import { type ProjectHoldings } from "./project-delete.js";
|
|
11
7
|
export declare const PROJECT_DELETE_CSS: string;
|
|
@@ -50,6 +46,27 @@ export type ProjectSettingsView = {
|
|
|
50
46
|
setting: WritesMode;
|
|
51
47
|
canChange: boolean;
|
|
52
48
|
};
|
|
49
|
+
/** What its builds and checks may not read, and the macOS services and protected files they may still use
|
|
50
|
+
* (project-sandbox.ts), and whether this person may change it. */
|
|
51
|
+
sandbox?: {
|
|
52
|
+
setting: ProjectSandbox;
|
|
53
|
+
canChange: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** How far its checks reach (check-access.ts): the level, who changed it last, and whether this person may narrow it
|
|
56
|
+
* (an approver for the project) or also widen it (an instance operator). */
|
|
57
|
+
checkAccess?: {
|
|
58
|
+
setting: CheckAccess;
|
|
59
|
+
changed: {
|
|
60
|
+
by: string;
|
|
61
|
+
at: string;
|
|
62
|
+
} | null;
|
|
63
|
+
canNarrow: boolean;
|
|
64
|
+
canWiden: boolean;
|
|
65
|
+
/** This machine can't sandbox checks at all, and an operator allowed them to run anyway: every level runs unsandboxed. */
|
|
66
|
+
unconfinedHere?: boolean;
|
|
67
|
+
/** Why sign-ins can't be chosen (off for now), or null: they can. */
|
|
68
|
+
signInsUnavailable?: string | null;
|
|
69
|
+
};
|
|
53
70
|
};
|
|
54
71
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
55
72
|
export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Project: what Toolroll holds for a project, and deleting
|
|
3
|
+
* it. An instance operator deletes in two steps: type the project's name,
|
|
4
|
+
* then read exactly what goes and confirm with their password (or a fresh
|
|
5
|
+
* sign-in with the identity provider). The repository itself stays.
|
|
6
|
+
*/
|
|
7
|
+
import { sandboxRuntimeWords } from "./sandbox-runtime.js";
|
|
8
|
+
import { CHECK_ACCESS_WARNINGS, signInHostsMissing } from "./check-access.js";
|
|
1
9
|
import { html, postForm, reauth } from "./html.js";
|
|
2
10
|
import { holdingsWords } from "./project-delete.js";
|
|
3
11
|
export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:720px;min-width:0}.project-settings .path,.project-delete .path{font-family:var(--font-mono);font-size:.8125rem;overflow-wrap:anywhere;word-break:break-all}` +
|
|
@@ -13,6 +21,21 @@ export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:7
|
|
|
13
21
|
`.project-settings .build-access .tool-name{font:var(--so-typography-mono)}.project-settings .build-access details{margin:4px 0 0}.project-settings .build-access summary{min-height:44px;display:inline-flex;align-items:center;cursor:pointer;color:var(--so-muted);font:var(--so-typography-label)}` +
|
|
14
22
|
`.project-settings .build-access ul{margin:0 0 4px;padding-left:20px;font:var(--so-typography-body);color:var(--so-ink)}.project-settings .build-access li{margin:2px 0}.project-settings .build-access li .meta{color:var(--so-muted)}` +
|
|
15
23
|
`.project-settings .build-access a.manage-tools{display:inline-flex;align-items:center;justify-content:center;min-height:44px;margin-top:4px;padding:0 14px;white-space:nowrap;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line);border-radius:var(--so-radius-md);font:var(--so-typography-label);text-decoration:none}` +
|
|
24
|
+
// A radio or checkbox choice sits beside its words, at its natural size, not as a full-width grid row.
|
|
25
|
+
`.project-settings .build-access label.check{display:flex;align-items:center;gap:8px;min-height:44px;font:var(--so-typography-body);font-weight:500}` +
|
|
26
|
+
`.project-settings .build-access label.check input{width:auto;min-height:0;margin:0;flex:none}.project-settings .build-access form .meta{margin:-8px 0 0 26px}` +
|
|
27
|
+
// Check access: a warning shows only under the option chosen; the folds draw the console's own chevron (as a diff's).
|
|
28
|
+
`.project-settings .check-access-setting .option .warning{display:none;color:var(--so-ink)}.project-settings .check-access-setting .option:has(input:checked) .warning{display:block}` +
|
|
29
|
+
`.project-settings .check-access-setting form{margin-top:0;gap:10px}.project-settings .check-access-setting .option{display:grid;gap:2px}` +
|
|
30
|
+
`.project-settings .check-access-setting .option .meta{margin:0 0 0 26px}.project-settings .check-access-setting label.check{min-height:36px}` +
|
|
31
|
+
`.project-settings .check-access-setting details > summary{list-style:none;display:flex;align-items:center;gap:10px;min-height:40px;padding:0}` +
|
|
32
|
+
`.project-settings .check-access-setting details > summary::-webkit-details-marker{display:none}.project-settings .check-access-setting details > summary::marker{content:""}` +
|
|
33
|
+
`.project-settings .check-access-setting details > summary::before{content:"";flex:none;width:.35rem;height:.35rem;border-right:1.5px solid var(--so-muted);border-bottom:1.5px solid var(--so-muted);transform:rotate(-45deg);transition:transform .15s ease}` +
|
|
34
|
+
`.project-settings .check-access-setting details[open] > summary::before{transform:rotate(45deg) translateY(-.1rem)}` +
|
|
35
|
+
// Show hosts: a quiet fold in the line, not a card.
|
|
36
|
+
`.project-settings .check-access-setting details.hosts{border:0;background:none;padding:0;margin:0}.project-settings .check-access-setting details.hosts > summary{min-height:32px;font-size:.8125rem}` +
|
|
37
|
+
`.project-settings .check-access-setting details.hosts .tool-name{margin:0 0 4px;font-size:.75rem}` +
|
|
38
|
+
`.project-settings .check-access-setting button.primary{background:var(--so-signal);color:var(--so-on-signal);border-color:var(--so-signal)}` +
|
|
16
39
|
`.project-settings .build-access a.manage-tools:hover{background:var(--so-raised)}.project-settings .build-access a.manage-tools:focus-visible,.project-settings .build-access summary:focus-visible{outline:2px solid var(--so-ink);outline-offset:2px}`;
|
|
17
40
|
/** Where builds may write, said plainly; an approver changes it with their password. */
|
|
18
41
|
function writesHtml(view) {
|
|
@@ -33,6 +56,76 @@ function writesHtml(view) {
|
|
|
33
56
|
<button type="submit">Save</button>`, { hidden: { repo: view.repo } });
|
|
34
57
|
return html `<section class="build-access writes-setting" id="writes" aria-labelledby="writes-title"><h2 id="writes-title">Where builds write</h2>${said}<details><summary>Change</summary>${form}</details></section>`;
|
|
35
58
|
}
|
|
59
|
+
/** Home folder (project-sandbox.ts): what builds and checks can read there, said once; the paths they can't, and the
|
|
60
|
+
* advanced list (macOS services), changed under Change with the approver's password. */
|
|
61
|
+
function sandboxHtml(view) {
|
|
62
|
+
const sandbox = view.sandbox;
|
|
63
|
+
if (sandbox === undefined)
|
|
64
|
+
return "";
|
|
65
|
+
const { readDeny, machLookup } = sandbox.setting;
|
|
66
|
+
const said = readDeny.length === 0
|
|
67
|
+
? html `<p>Builds and checks can read your home folder, as in Anthropic's sandbox runtime. Toolroll's own folders stay out of reach; other tasks' work is read, never written.</p>`
|
|
68
|
+
: html `<p>Builds and checks can read your home folder except <strong>${readDeny.length === 1 ? readDeny[0] : `${readDeny.length} paths`}</strong>.</p>`;
|
|
69
|
+
const extra = machLookup.length === 0 ? "" : html `<p class="meta">${machLookup.length} macOS ${machLookup.length === 1 ? "service" : "services"} allowed</p>`;
|
|
70
|
+
// The engine, named where the sandbox is set (sandbox-runtime.ts).
|
|
71
|
+
const engine = html `<p class="meta" data-sandbox-engine>Builds, checks and flows run in ${sandboxRuntimeWords()}.</p>`;
|
|
72
|
+
const head = html `<h2 id="sandbox-title">Home folder</h2>${said}${extra}${engine}`;
|
|
73
|
+
if (!sandbox.canChange)
|
|
74
|
+
return html `<section class="build-access sandbox-setting" id="sandbox" aria-labelledby="sandbox-title">${head}<p class="meta">An approver for this project can change this.</p></section>`;
|
|
75
|
+
const form = postForm("/settings/project/sandbox", html `
|
|
76
|
+
<label>Paths builds can't read<textarea name="read-deny" rows="4" placeholder="~/.ssh ~/.aws">${readDeny.join("\n")}</textarea></label>
|
|
77
|
+
<p class="meta">One per line: <code>~/…</code> or a full path.</p>
|
|
78
|
+
<details><summary>Advanced</summary>
|
|
79
|
+
<label>macOS services builds may also use<textarea name="mach" rows="2">${machLookup.join("\n")}</textarea></label>
|
|
80
|
+
</details>
|
|
81
|
+
<p class="meta">Tasks scoped from now on use this; tasks already approved keep what they were approved with.</p>
|
|
82
|
+
${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
|
|
83
|
+
<button type="submit">Save</button>`, { hidden: { repo: view.repo } });
|
|
84
|
+
return html `<section class="build-access sandbox-setting" id="sandbox" aria-labelledby="sandbox-title">${head}<details><summary>Change</summary>${form}</details></section>`;
|
|
85
|
+
}
|
|
86
|
+
/** Check access (check-access.ts): how far the project's checks reach, said once. Collapsed: the level, what it means
|
|
87
|
+
* when it isn't the default, and who changed it. Inside Change: a line per level, and each wider level's warning under it
|
|
88
|
+
* only while it is chosen. An instance operator widens it with a password typed now; an approver narrows it. */
|
|
89
|
+
function checkAccessHtml(view) {
|
|
90
|
+
const access = view.checkAccess;
|
|
91
|
+
if (access === undefined)
|
|
92
|
+
return "";
|
|
93
|
+
const saved = access.setting;
|
|
94
|
+
// Sign-ins aren't available (off for now): a saved sign-ins level runs sandboxed, and the page says so.
|
|
95
|
+
const unavailable = access.signInsUnavailable ?? null;
|
|
96
|
+
const setting = unavailable !== null && saved === "sign-ins" ? "sandboxed" : saved;
|
|
97
|
+
const network = view.network?.setting ?? null;
|
|
98
|
+
const names = { sandboxed: "Sandboxed", "sign-ins": "Sandboxed, with model and GitHub sign-ins", unsandboxed: "Not sandboxed" };
|
|
99
|
+
const meaning = { sandboxed: null, "sign-ins": "Checks can read your Claude, Codex and GitHub sign-ins.", unsandboxed: "Checks run with your full access to this machine and to Toolroll." };
|
|
100
|
+
// The sign-ins never widen the network: how many hosts they'd still need, and where to add them (a change of its own).
|
|
101
|
+
const missing = setting === "sign-ins" && network !== null ? signInHostsMissing(network) : [];
|
|
102
|
+
const hosts = missing.length === 0 ? "" : html `<p class="meta" data-sign-in-hosts>${missing.length} sign-in ${missing.length === 1 ? "host isn't" : "hosts aren't"} allowed yet · <a href="#network">Add them</a></p><details class="hosts"><summary>Show hosts</summary><p class="tool-name">${missing.join(", ")}</p></details>`;
|
|
103
|
+
const changed = access.changed === null ? "" : html `<p class="meta">Changed by ${access.changed.by}, ${new Date(access.changed.at).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })}</p>`;
|
|
104
|
+
// Said before anything else: on a machine with no OS sandbox, every level runs without one.
|
|
105
|
+
const unconfined = access.unconfinedHere === true ? html `<p class="meta warning-line" data-unconfined-here>This machine can't sandbox checks, so they run without a sandbox whatever is chosen here.</p>` : "";
|
|
106
|
+
const runsSandboxed = setting !== saved && access.unconfinedHere !== true ? html `<p class="meta warning-line" data-sign-ins-unavailable>Saved with model and GitHub sign-ins, but checks run sandboxed: sign-ins can't be shared with checks safely yet.</p>` : "";
|
|
107
|
+
// No OS sandbox here, and unconfined running allowed: checks run without a sandbox whatever is saved, and that is
|
|
108
|
+
// what the page says first (never "sandboxed"); the saved level is named under it.
|
|
109
|
+
const head = access.unconfinedHere === true
|
|
110
|
+
? html `<h2 id="check-access-title">Check access</h2><p><strong>${names.unsandboxed}</strong></p>${saved === "unsandboxed" ? "" : html `<p class="meta" data-saved-level>Saved as ${names[saved]}.</p>`}${unconfined}${changed}`
|
|
111
|
+
: html `<h2 id="check-access-title">Check access</h2><p><strong>${names[setting]}</strong></p>${meaning[setting] === null ? "" : html `<p class="meta">${meaning[setting]}</p>`}${runsSandboxed}${hosts}${changed}`;
|
|
112
|
+
if (!access.canNarrow && !access.canWiden)
|
|
113
|
+
return html `<section class="build-access check-access-setting" id="check-access" aria-labelledby="check-access-title">${head}</section>`;
|
|
114
|
+
const option = (value, detail, warning) => {
|
|
115
|
+
const allowed = (value === setting || access.canWiden || value === "sandboxed") && !(value === "sign-ins" && unavailable !== null);
|
|
116
|
+
return html `<div class="option"><label class="check"><input type="radio" name="level" value="${value}"${setting === value ? html ` checked` : ""}${allowed ? "" : html ` disabled`}> ${names[value]}</label><p class="meta">${detail}</p>${warning === null ? "" : html `<p class="meta warning" data-check-access-warning="${value}">${warning}</p>`}</div>`;
|
|
117
|
+
};
|
|
118
|
+
const open = network?.mode === "open";
|
|
119
|
+
const form = postForm("/settings/project/check-access", html `
|
|
120
|
+
${option("sandboxed", "Reads your home folder, as in Anthropic's sandbox runtime, except the folders this project hides (Home folder, above). Never Toolroll's folders or other tasks' work.", null)}
|
|
121
|
+
${unavailable !== null ? option("sign-ins", unavailable, null) : option("sign-ins", `Also reads the files your Claude, Codex and GitHub sign-ins are kept in, on this project's network (${open ? "any host" : network === null || network.hosts.length === 0 ? "registries only" : `registries and ${network.hosts.length} ${network.hosts.length === 1 ? "host" : "hosts"}`}).`, CHECK_ACCESS_WARNINGS["sign-ins"])}
|
|
122
|
+
${option("unsandboxed", "For projects whose checks create sandboxes themselves, such as Toolroll's own.", CHECK_ACCESS_WARNINGS.unsandboxed)}
|
|
123
|
+
${open && unavailable === null ? html `<label class="check"><input type="checkbox" name="confirm-open" value="yes"> The network is open: with sign-ins, checks could send them to any host</label>` : ""}
|
|
124
|
+
${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
|
|
125
|
+
<button type="submit" class="primary">Save</button>
|
|
126
|
+
<p class="meta">${access.canWiden ? "" : "Only an instance operator can widen this. "}Flow script steps always run sandboxed. Tasks already approved keep their level.</p>`, { hidden: { repo: view.repo } });
|
|
127
|
+
return html `<section class="build-access check-access-setting" id="check-access" aria-labelledby="check-access-title">${head}<details class="change"><summary>Change</summary>${form}</details></section>`;
|
|
128
|
+
}
|
|
36
129
|
/** Network: what the project's builds may reach, said plainly; an approver changes it with their password. */
|
|
37
130
|
function networkHtml(view) {
|
|
38
131
|
const network = view.network;
|
|
@@ -111,7 +204,7 @@ export function projectSettingsHtml(view, notice) {
|
|
|
111
204
|
const remote = html `<section class="build-access recorded-remote" aria-labelledby="recorded-remote-title"><h2 id="recorded-remote-title">Recorded remote</h2>${view.remoteUrl
|
|
112
205
|
? html `<p class="path">${view.remoteUrl}</p>`
|
|
113
206
|
: html `<p>No remote recorded. Landed work waits for your review.</p>`}<details><summary>Change remote</summary><p>An owner or instance operator can run:</p><p class="path">toolroll repos add <project> --remote <url></p></details></section>`;
|
|
114
|
-
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${remote}${buildsHtml(view)}${accessHtml(view)}${networkHtml(view)}${writesHtml(view)}${liveViewHtml(view)}`;
|
|
207
|
+
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${remote}${buildsHtml(view)}${accessHtml(view)}${networkHtml(view)}${writesHtml(view)}${sandboxHtml(view)}${checkAccessHtml(view)}${liveViewHtml(view)}`;
|
|
115
208
|
if (!view.canDelete)
|
|
116
209
|
return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
|
|
117
210
|
const body = view.running.length > 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ProjectSandbox = {
|
|
2
|
+
readDeny: string[];
|
|
3
|
+
machLookup: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare const PROJECT_SANDBOX_DEFAULT: ProjectSandbox;
|
|
6
|
+
export declare const MAX_READ_DENY = 100;
|
|
7
|
+
export declare const MAX_MACH_LOOKUP = 30;
|
|
8
|
+
/** A read-deny entry: `~/…` (the home folder of whoever runs the build) or an absolute path; normalized, no `..`, no
|
|
9
|
+
* pattern; `~` or `/` alone never (that would deny everything a build needs). */
|
|
10
|
+
export declare function validReadDeny(one: string): boolean;
|
|
11
|
+
/** Services no project can add, whatever it asks: the keychain and security services, the pasteboard, Launch Services
|
|
12
|
+
* and Apple Events (what a build would open or script another application with), the privacy database, the window
|
|
13
|
+
* server. Each name or prefix here matches the service and everything beneath it. */
|
|
14
|
+
export declare const MACH_NEVER: readonly string[];
|
|
15
|
+
/** A mach service a project may add: an exact reverse-DNS name, or a prefix ending `.*` after at least three labels
|
|
16
|
+
* (`com.example.tool.*`); never one of MACH_NEVER, nor a prefix that would reach one. */
|
|
17
|
+
export declare function validMachLookup(one: string): boolean;
|
|
18
|
+
/** Entries as a setting keeps them: trimmed, distinct, sorted; refused whole when any is invalid or there are too many. */
|
|
19
|
+
export declare function parseSandboxList(lines: readonly string[], valid: (one: string) => boolean, max: number): {
|
|
20
|
+
ok: true;
|
|
21
|
+
list: string[];
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
problem: string;
|
|
25
|
+
};
|
|
26
|
+
export declare const parseReadDeny: (lines: readonly string[]) => {
|
|
27
|
+
ok: true;
|
|
28
|
+
list: string[];
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
problem: string;
|
|
32
|
+
};
|
|
33
|
+
export declare const parseMachLookup: (lines: readonly string[]) => {
|
|
34
|
+
ok: true;
|
|
35
|
+
list: string[];
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
problem: string;
|
|
39
|
+
};
|
|
40
|
+
/** A stored or sealed setting, read strictly: anything unreadable reads as the narrowest (no extra services) but keeps
|
|
41
|
+
* whatever read-denies are valid (never fewer than were asked for). */
|
|
42
|
+
export declare function sandboxOf(raw: {
|
|
43
|
+
readDeny?: unknown;
|
|
44
|
+
machLookup?: unknown;
|
|
45
|
+
}): ProjectSandbox;
|
|
46
|
+
/** A read-deny entry as the path it names for a launch whose home folder is `home`. */
|
|
47
|
+
export declare const readDenyPath: (one: string, home: string) => string;
|
|
48
|
+
/** What an approval's sandbox terms say, one line each, for every surface that asks for a yes (scope.ts describeScope,
|
|
49
|
+
* the console's scope view): the fence policy and its read denies always, the macOS services when there are any. */
|
|
50
|
+
export declare function sandboxTermsWords(profile: {
|
|
51
|
+
fencePolicy?: 2;
|
|
52
|
+
readDeny?: readonly string[];
|
|
53
|
+
machLookup?: readonly string[];
|
|
54
|
+
}): {
|
|
55
|
+
reads: string;
|
|
56
|
+
mach: string | null;
|
|
57
|
+
};
|
|
58
|
+
/** The setting in a sentence, for the CLI and the settings page. */
|
|
59
|
+
export declare function readDenyWords(list: readonly string[]): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's sandbox settings (docs/plans/sandbox-parity.md C9), an approver's, each sealed into new scopes:
|
|
3
|
+
*
|
|
4
|
+
* - `readDeny`: what its builds and checks may not read (Anthropic's sandbox runtime `denyRead`). Under fence policy v2
|
|
5
|
+
* the home folder is readable as in the sandbox runtime; this is how a project hides `~/.ssh` and the like again.
|
|
6
|
+
* Narrowing only: it applies to running launches' next start too (flows, coding sessions read it live).
|
|
7
|
+
* - `machLookup`: macOS services its builds may also reach (the sandbox runtime `allowMachLookup`), for toolchains the
|
|
8
|
+
* deny-default baseline breaks (Go's TLS, Playwright). Widening: sealed, and only a person's password yes seals it.
|
|
9
|
+
*/
|
|
10
|
+
import { isAbsolute, normalize } from "node:path";
|
|
11
|
+
export const PROJECT_SANDBOX_DEFAULT = { readDeny: [], machLookup: [] };
|
|
12
|
+
export const MAX_READ_DENY = 100;
|
|
13
|
+
export const MAX_MACH_LOOKUP = 30;
|
|
14
|
+
/** A read-deny entry: `~/…` (the home folder of whoever runs the build) or an absolute path; normalized, no `..`, no
|
|
15
|
+
* pattern; `~` or `/` alone never (that would deny everything a build needs). */
|
|
16
|
+
export function validReadDeny(one) {
|
|
17
|
+
if (one.length === 0 || one.length > 400 || one.includes("\0") || /[*?[\]{}]/.test(one))
|
|
18
|
+
return false;
|
|
19
|
+
const path = one.startsWith("~/") ? one.slice(2) : isAbsolute(one) ? one.slice(1) : null;
|
|
20
|
+
if (path === null || path === "" || path.endsWith("/"))
|
|
21
|
+
return false;
|
|
22
|
+
return path.split("/").every(part => part !== "" && part !== "." && part !== "..") && normalize(`/${path}`) === `/${path}`;
|
|
23
|
+
}
|
|
24
|
+
/** Services no project can add, whatever it asks: the keychain and security services, the pasteboard, Launch Services
|
|
25
|
+
* and Apple Events (what a build would open or script another application with), the privacy database, the window
|
|
26
|
+
* server. Each name or prefix here matches the service and everything beneath it. */
|
|
27
|
+
export const MACH_NEVER = ["com.apple.security", "com.apple.SecurityServer", "com.apple.securityd", "com.apple.pasteboard",
|
|
28
|
+
"com.apple.coreservices.launchservicesd", "com.apple.lsd", "com.apple.coreservices.appleevents", "com.apple.tccd", "com.apple.windowserver",
|
|
29
|
+
"com.apple.CoreServices.coreservicesd"];
|
|
30
|
+
/** A mach service a project may add: an exact reverse-DNS name, or a prefix ending `.*` after at least three labels
|
|
31
|
+
* (`com.example.tool.*`); never one of MACH_NEVER, nor a prefix that would reach one. */
|
|
32
|
+
export function validMachLookup(one) {
|
|
33
|
+
if (one.length > 200)
|
|
34
|
+
return false;
|
|
35
|
+
const prefix = one.endsWith(".*");
|
|
36
|
+
const name = prefix ? one.slice(0, -2) : one;
|
|
37
|
+
const labels = name.split(".");
|
|
38
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*(\.[A-Za-z0-9_@-]+)+$/.test(name) || labels.length < (prefix ? 3 : 2))
|
|
39
|
+
return false;
|
|
40
|
+
const lower = name.toLowerCase();
|
|
41
|
+
return !MACH_NEVER.some(never => { const blocked = never.toLowerCase(); return lower === blocked || lower.startsWith(blocked) || (prefix && blocked.startsWith(`${lower}.`)); });
|
|
42
|
+
}
|
|
43
|
+
/** Entries as a setting keeps them: trimmed, distinct, sorted; refused whole when any is invalid or there are too many. */
|
|
44
|
+
export function parseSandboxList(lines, valid, max) {
|
|
45
|
+
const list = [...new Set(lines.map(one => one.trim()).filter(one => one !== ""))].sort();
|
|
46
|
+
const bad = list.find(one => !valid(one));
|
|
47
|
+
if (bad !== undefined)
|
|
48
|
+
return { ok: false, problem: `“${bad.slice(0, 80)}” isn't a valid entry` };
|
|
49
|
+
if (list.length > max)
|
|
50
|
+
return { ok: false, problem: `at most ${max} entries` };
|
|
51
|
+
return { ok: true, list };
|
|
52
|
+
}
|
|
53
|
+
export const parseReadDeny = (lines) => parseSandboxList(lines, validReadDeny, MAX_READ_DENY);
|
|
54
|
+
export const parseMachLookup = (lines) => parseSandboxList(lines, validMachLookup, MAX_MACH_LOOKUP);
|
|
55
|
+
/** A stored or sealed setting, read strictly: anything unreadable reads as the narrowest (no extra services) but keeps
|
|
56
|
+
* whatever read-denies are valid (never fewer than were asked for). */
|
|
57
|
+
export function sandboxOf(raw) {
|
|
58
|
+
const strings = (value) => Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
|
|
59
|
+
const readDeny = parseReadDeny(strings(raw.readDeny));
|
|
60
|
+
const mach = parseMachLookup(strings(raw.machLookup));
|
|
61
|
+
return {
|
|
62
|
+
readDeny: readDeny.ok ? readDeny.list : strings(raw.readDeny).filter(validReadDeny),
|
|
63
|
+
machLookup: mach.ok ? mach.list : [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** A read-deny entry as the path it names for a launch whose home folder is `home`. */
|
|
67
|
+
export const readDenyPath = (one, home) => one.startsWith("~/") ? `${home}/${one.slice(2)}` : one;
|
|
68
|
+
/** What an approval's sandbox terms say, one line each, for every surface that asks for a yes (scope.ts describeScope,
|
|
69
|
+
* the console's scope view): the fence policy and its read denies always, the macOS services when there are any. */
|
|
70
|
+
export function sandboxTermsWords(profile) {
|
|
71
|
+
const reads = profile.fencePolicy === 2
|
|
72
|
+
? `your home folder, as in Anthropic's sandbox runtime${(profile.readDeny ?? []).length === 0 ? "" : `, except ${(profile.readDeny ?? []).join(", ")}`}; never Toolroll's own folders; other tasks' work read, never written`
|
|
73
|
+
: "not your home folder's credentials and agent settings (approved before home reads opened)";
|
|
74
|
+
return {
|
|
75
|
+
reads,
|
|
76
|
+
mach: (profile.machLookup ?? []).length === 0 ? null : `also reaches these macOS services: ${(profile.machLookup ?? []).join(", ")}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The setting in a sentence, for the CLI and the settings page. */
|
|
80
|
+
export function readDenyWords(list) {
|
|
81
|
+
return list.length === 0 ? "Builds and checks can read your home folder, as in Anthropic's sandbox runtime; Toolroll's own folders stay out of reach, and other tasks' work is read, never written."
|
|
82
|
+
: `Builds and checks can read your home folder except ${list.length === 1 ? list[0] : `${list.length} paths (${list.slice(0, 3).join(", ")}${list.length > 3 ? ", …" : ""})`}.`;
|
|
83
|
+
}
|
package/dist/project-tools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Store, ToolActionInfo } from "./store.js";
|
|
2
2
|
import { type BrokerOptions } from "./tool-broker.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type FenceMethod, type LaunchFence } from "./agent-fence.js";
|
|
4
4
|
import type { ProviderId } from "./contracts/provider.js";
|
|
5
5
|
import { type SecretStoreKind } from "./tool-secret-store.js";
|
|
6
6
|
import { type ToolSecret, type ToolSpec } from "./contracts/integration-metadata.js";
|
|
@@ -105,10 +105,11 @@ export type FoundTool = {
|
|
|
105
105
|
*/
|
|
106
106
|
export declare function discoverTools(repo: string, codexServers: unknown[] | null, home?: string, environment?: NodeJS.ProcessEnv): FoundTool[];
|
|
107
107
|
/** Start the server once, ask for its tools, and stop it: the proof a build can use it. */
|
|
108
|
-
export declare function testTool(spec: ToolSpec, values: Record<string, string>, options
|
|
108
|
+
export declare function testTool(spec: ToolSpec, values: Record<string, string>, options: {
|
|
109
109
|
timeoutMs?: number;
|
|
110
110
|
env?: NodeJS.ProcessEnv;
|
|
111
111
|
omitEnv?: readonly string[];
|
|
112
|
+
server: ToolServer;
|
|
112
113
|
}): Promise<{
|
|
113
114
|
ok: true;
|
|
114
115
|
tools: string[];
|
|
@@ -121,10 +122,11 @@ export declare function testTool(spec: ToolSpec, values: Record<string, string>,
|
|
|
121
122
|
* its description, what input it takes, and whether it says it only reads.
|
|
122
123
|
* A subagent is offered these, each under its own rule.
|
|
123
124
|
*/
|
|
124
|
-
export declare function listProjectToolActions(spec: ToolSpec, values: Record<string, string>, options
|
|
125
|
+
export declare function listProjectToolActions(spec: ToolSpec, values: Record<string, string>, options: {
|
|
125
126
|
timeoutMs?: number;
|
|
126
127
|
env?: NodeJS.ProcessEnv;
|
|
127
128
|
omitEnv?: readonly string[];
|
|
129
|
+
server: ToolServer;
|
|
128
130
|
}): Promise<{
|
|
129
131
|
ok: true;
|
|
130
132
|
actions: ToolActionInfo[];
|
|
@@ -146,10 +148,11 @@ export type ToolCall = {
|
|
|
146
148
|
* "Use a tool" step. The answer's text parts are kept; a tool that says it
|
|
147
149
|
* failed (isError) is an answer, not trouble reaching it.
|
|
148
150
|
*/
|
|
149
|
-
export declare function callProjectTool(spec: ToolSpec, values: Record<string, string>, name: string, args: Record<string, unknown>, options
|
|
151
|
+
export declare function callProjectTool(spec: ToolSpec, values: Record<string, string>, name: string, args: Record<string, unknown>, options: {
|
|
150
152
|
timeoutMs?: number;
|
|
151
153
|
env?: NodeJS.ProcessEnv;
|
|
152
154
|
omitEnv?: readonly string[];
|
|
155
|
+
server: ToolServer;
|
|
153
156
|
}): Promise<ToolCall>;
|
|
154
157
|
/** The exact servers one launch gets, and those left out with why (`repo`: the project, from the run's record). */
|
|
155
158
|
export type ToolLaunch = {
|
|
@@ -259,10 +262,11 @@ export declare function addToolTo(store: ToolWrites, repo: string, input: ToolSp
|
|
|
259
262
|
/** Take one tool away from every build now, with its stored secrets. */
|
|
260
263
|
export declare function removeToolFrom(store: ToolWrites, repo: string, name: string, by: string, now: Date, home?: string): boolean;
|
|
261
264
|
/** Test one tool of a project and keep the answer on it. */
|
|
262
|
-
export declare function testToolOf(store: ToolWrites, repo: string, name: string, now: Date, options?: {
|
|
265
|
+
export declare function testToolOf(store: ToolWrites & ToolServerStore, repo: string, name: string, now: Date, options?: {
|
|
263
266
|
home?: string | undefined;
|
|
264
267
|
timeoutMs?: number;
|
|
265
268
|
omitEnv?: readonly string[];
|
|
269
|
+
available?: () => boolean;
|
|
266
270
|
}): Promise<ToolTest | null>;
|
|
267
271
|
/** Where a tool stands, in a few words: what a person reads first. */
|
|
268
272
|
export declare function toolStanding(tool: ProjectTool, secretsSet: readonly string[]): {
|
|
@@ -289,3 +293,23 @@ export declare function toolSecretStorage(): {
|
|
|
289
293
|
};
|
|
290
294
|
/** A command line split into program and arguments, honouring "double" and 'single' quotes. */
|
|
291
295
|
export declare function splitCommandLine(line: string): string[];
|
|
296
|
+
/** What a tool server started outside a build needs of the store: the plane's own records of its state and pools. */
|
|
297
|
+
export type ToolServerStore = Pick<Store, "databaseFile" | "listWorktrees" | "unfencedAgentsAllowed">;
|
|
298
|
+
/** How a tool server started outside a build runs (toolServerFor): inside `fence`, or with none where there is nothing to
|
|
299
|
+
* fence (a store with no database file) or an instance operator allowed unconfined agents; or not at all. */
|
|
300
|
+
export type ToolServer = {
|
|
301
|
+
fence: LaunchFence | null;
|
|
302
|
+
} | {
|
|
303
|
+
problem: string;
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* The server fence for a project tool started outside a build (docs/plans/sandbox-parity.md C2): discovery, its Test
|
|
307
|
+
* button, a flow's "Use a tool" step, a subagent's call. As the broker's servers are fenced: Toolroll's state, every
|
|
308
|
+
* other launch's folder and the checkout pools denied, and nothing a build writes (its project's caches) read, so no
|
|
309
|
+
* byte a build wrote can load into a process that holds the tool's secrets. Required: where this machine can't confine
|
|
310
|
+
* it, it doesn't start unless an instance operator allowed unconfined agents.
|
|
311
|
+
*/
|
|
312
|
+
export declare function toolServerFor(store: ToolServerStore, repo: string, options?: {
|
|
313
|
+
home?: string;
|
|
314
|
+
available?: () => boolean;
|
|
315
|
+
}): ToolServer;
|