toolroll 0.9.60 → 0.9.62
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 +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/orphan-sweep.js
CHANGED
|
@@ -180,7 +180,8 @@ export async function sweepOrphans(store, now, options = {}) {
|
|
|
180
180
|
catch {
|
|
181
181
|
return { stopped: [], failed: ["the process list couldn't be read"] };
|
|
182
182
|
}
|
|
183
|
-
const
|
|
183
|
+
const rootsNow = (rows) => sweepRoots(store, options.pool ?? null, rows, options.tempRoots ?? [], now()).filter(root => options.spare?.(root.path) !== true);
|
|
184
|
+
const roots = rootsNow(census);
|
|
184
185
|
if (roots.length === 0)
|
|
185
186
|
return { stopped: [], failed };
|
|
186
187
|
const guard = protectedProcesses(census, options.self);
|
|
@@ -195,14 +196,15 @@ export async function sweepOrphans(store, now, options = {}) {
|
|
|
195
196
|
const first = orphanTargets(census, located, roots, guard);
|
|
196
197
|
if (first.length === 0)
|
|
197
198
|
return { stopped: [], failed };
|
|
198
|
-
// Right before the signal, everything again: the same process, still an orphan (or under one), still inside
|
|
199
|
+
// Right before the signal, everything again: the same process, still an orphan (or under one), still inside a root
|
|
200
|
+
// that is still one (no run or lease took its checkout since, nothing live uses it).
|
|
199
201
|
let again;
|
|
200
202
|
try {
|
|
201
203
|
const fresh = read();
|
|
202
204
|
const freshGuard = protectedProcesses(fresh, options.self);
|
|
203
205
|
const freshLocated = locate(fresh.filter(row => row.ppid === 1).map(row => row.pid));
|
|
204
206
|
const before = new Map(first.map(one => [one.row.pid, one]));
|
|
205
|
-
again = orphanTargets(fresh, freshLocated,
|
|
207
|
+
again = orphanTargets(fresh, freshLocated, rootsNow(fresh), freshGuard).filter(one => {
|
|
206
208
|
const was = before.get(one.row.pid);
|
|
207
209
|
return was !== undefined && Math.abs(was.row.bornAt - one.row.bornAt) <= BIRTH_SLACK_MS && was.root.path === one.root.path;
|
|
208
210
|
});
|
|
@@ -24,11 +24,16 @@ export type AdmissionFailure = {
|
|
|
24
24
|
reason: "unavailable" | "not-git" | "locked" | "registry" | "abandoned" | "failed";
|
|
25
25
|
message: string;
|
|
26
26
|
};
|
|
27
|
+
/** `remote`: the origin URL this add recorded as the project's trusted remote (null: none), and why one wasn't. */
|
|
27
28
|
export type Admitted = {
|
|
28
29
|
ok: true;
|
|
29
30
|
repo: string;
|
|
30
31
|
name: string;
|
|
31
32
|
added: boolean;
|
|
33
|
+
remote: {
|
|
34
|
+
url: string | null;
|
|
35
|
+
why: string | null;
|
|
36
|
+
};
|
|
32
37
|
};
|
|
33
38
|
export type Released = {
|
|
34
39
|
ok: true;
|
|
@@ -43,12 +48,7 @@ export declare function proveProjectRoot(path: string, exec?: ProjectExec): Prom
|
|
|
43
48
|
ok: true;
|
|
44
49
|
repo: string;
|
|
45
50
|
} | AdmissionFailure>;
|
|
46
|
-
/**
|
|
47
|
-
* Admit one project. Idempotent: adding a saved project again changes
|
|
48
|
-
* nothing and writes no ledger row. The registry is written first, under
|
|
49
|
-
* its lock; the project row and its ledger row then commit together, so a
|
|
50
|
-
* retry after a failure finishes the job and records it exactly once.
|
|
51
|
-
*/
|
|
51
|
+
/** Registry first, then the project and its audit records in one transaction. Re-adds preserve trust. */
|
|
52
52
|
export declare function admitProject(store: Store, input: {
|
|
53
53
|
/** null: a console with no machine registry (a bare `serve`) keeps only the project list. */
|
|
54
54
|
registryFile: string | null;
|
|
@@ -56,6 +56,10 @@ export declare function admitProject(store: Store, input: {
|
|
|
56
56
|
actor: AdmissionActor;
|
|
57
57
|
now: Date;
|
|
58
58
|
exec?: ProjectExec;
|
|
59
|
+
/** Only the authorized CLI add opts in; console/open/onboarding never discover a remote. */
|
|
60
|
+
captureOrigin?: boolean;
|
|
61
|
+
/** Explicit value from the owner/instance-operator CLI boundary. */
|
|
62
|
+
remoteUrl?: string;
|
|
59
63
|
}): Promise<Admitted | AdmissionFailure>;
|
|
60
64
|
/**
|
|
61
65
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -15,7 +15,12 @@ import { userInfo } from "node:os";
|
|
|
15
15
|
import { dirname, join, resolve } from "node:path";
|
|
16
16
|
import { run } from "./exec.js";
|
|
17
17
|
import { canonicalProject, projectName, sameRepo } from "./project.js";
|
|
18
|
+
import { remoteUrlProblem } from "./remote-main.js";
|
|
18
19
|
import { addRepos, loadProjectRegistry, removeRepos, updateRepos } from "./repos.js";
|
|
20
|
+
const ADMISSION_GIT = {
|
|
21
|
+
envAllowlist: ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"],
|
|
22
|
+
env: { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_NO_REPLACE_OBJECTS: "1" },
|
|
23
|
+
};
|
|
19
24
|
export const PROJECT_ADDED = "project added";
|
|
20
25
|
export const PROJECT_REMOVED = "project removed";
|
|
21
26
|
/** The checkout root a folder belongs to, canonical; never a shell. */
|
|
@@ -26,40 +31,57 @@ export async function proveProjectRoot(path, exec = run) {
|
|
|
26
31
|
const canonical = canonicalProject(path);
|
|
27
32
|
if (canonical === null)
|
|
28
33
|
return { ok: false, reason: "unavailable", message: `${path} isn't a folder on this computer.` };
|
|
29
|
-
const top = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: canonical, timeoutMs: 5_000 });
|
|
34
|
+
const top = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: canonical, timeoutMs: 5_000, ...ADMISSION_GIT });
|
|
30
35
|
const repo = top.code === 0 ? canonicalProject(top.stdout.trim()) : null;
|
|
31
36
|
if (repo === null)
|
|
32
37
|
return { ok: false, reason: "not-git", message: `${path} isn't a Git repository. Run git init there first, or choose a folder inside one.` };
|
|
33
38
|
return { ok: true, repo };
|
|
34
39
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
/** Read only on first CLI admission; never rediscover trust from an existing project's config. */
|
|
41
|
+
async function originOf(repo, exec) {
|
|
42
|
+
const read = await exec("git", ["config", "--local", "--no-includes", "--get", "remote.origin.url"], { cwd: repo, timeoutMs: 5_000, ...ADMISSION_GIT });
|
|
43
|
+
const url = read.code === 0 ? read.stdout.trim() : "";
|
|
44
|
+
if (url === "")
|
|
45
|
+
return { url: null, why: "it has no origin remote" };
|
|
46
|
+
const problem = remoteUrlProblem(url);
|
|
47
|
+
return problem === null ? { url, why: null } : { url: null, why: problem };
|
|
48
|
+
}
|
|
49
|
+
/** Registry first, then the project and its audit records in one transaction. Re-adds preserve trust. */
|
|
41
50
|
export async function admitProject(store, input) {
|
|
42
51
|
const proved = await proveProjectRoot(input.path, input.exec);
|
|
43
52
|
if (!proved.ok)
|
|
44
53
|
return proved;
|
|
45
54
|
const { repo } = proved;
|
|
55
|
+
if (input.remoteUrl !== undefined && (input.actor.origin === "console" || remoteUrlProblem(input.remoteUrl) !== null)) {
|
|
56
|
+
return { ok: false, reason: "failed", message: "Only an owner or instance operator can record a network remote with repos add --remote <url>. Use an HTTPS or SSH URL without credentials." };
|
|
57
|
+
}
|
|
58
|
+
const before = input.registryFile === null ? null : await loadProjectRegistry(input.registryFile);
|
|
59
|
+
const known = store.projectAdmissionKnown(repo) || (before !== null && !("error" in before) && before.repos.includes(repo));
|
|
60
|
+
const discovered = !known && input.captureOrigin === true && input.actor.origin !== "console"
|
|
61
|
+
? await originOf(repo, input.exec ?? run) : null;
|
|
62
|
+
let remote = { url: store.projectRemote(repo), why: null };
|
|
46
63
|
let registryHad = true;
|
|
47
64
|
if (input.registryFile !== null) {
|
|
48
|
-
const before = await loadProjectRegistry(input.registryFile);
|
|
49
65
|
const enrolled = await updateRepos(input.registryFile, repos => addRepos(repos, [repo]));
|
|
50
66
|
if (!enrolled.ok)
|
|
51
67
|
return { ok: false, reason: enrolled.reason, message: `${projectName(repo)} wasn't added: ${enrolled.message}` };
|
|
52
|
-
registryHad = !("error" in before) && before.repos.includes(repo);
|
|
68
|
+
registryHad = before !== null && !("error" in before) && before.repos.includes(repo);
|
|
53
69
|
}
|
|
54
70
|
const added = store.transact(() => {
|
|
55
71
|
const rowHad = store.listProjects().some(one => one.path === repo);
|
|
56
|
-
store.
|
|
72
|
+
const previous = store.projectRemote(repo);
|
|
73
|
+
const first = !known && !store.projectAdmissionKnown(repo);
|
|
74
|
+
remote = input.remoteUrl !== undefined ? { url: input.remoteUrl, why: null }
|
|
75
|
+
: first && discovered !== null ? discovered : { url: previous, why: previous === null ? "no remote recorded; use repos add --remote <url>" : null };
|
|
76
|
+
store.upsertProject(repo, projectName(repo), input.now, { remoteUrl: remote.url });
|
|
77
|
+
if (input.remoteUrl !== undefined)
|
|
78
|
+
store.recordProjectRemote(input.actor.label, repo, previous, input.remoteUrl, input.now);
|
|
57
79
|
if (rowHad && registryHad)
|
|
58
80
|
return false;
|
|
59
81
|
store.recordAction({ at: input.now.toISOString(), actor: input.actor.label, repo, taskId: null, runId: null, action: PROJECT_ADDED, outcome: "added", source: "access", detail: `from ${input.actor.origin}` });
|
|
60
82
|
return true;
|
|
61
83
|
});
|
|
62
|
-
return { ok: true, repo, name: projectName(repo), added };
|
|
84
|
+
return { ok: true, repo, name: projectName(repo), added, remote };
|
|
63
85
|
}
|
|
64
86
|
/**
|
|
65
87
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -8,6 +8,7 @@ import { type Html } from "./html.js";
|
|
|
8
8
|
import { type ProjectHoldings } from "./project-delete.js";
|
|
9
9
|
export declare const PROJECT_DELETE_CSS: string;
|
|
10
10
|
export type ProjectSettingsView = {
|
|
11
|
+
remoteUrl?: string | null;
|
|
11
12
|
repo: string;
|
|
12
13
|
name: string;
|
|
13
14
|
holdings: ProjectHoldings;
|
|
@@ -20,6 +21,17 @@ export type ProjectSettingsView = {
|
|
|
20
21
|
building: number;
|
|
21
22
|
canChange: boolean;
|
|
22
23
|
};
|
|
24
|
+
/** What its builds can reach: the project's named tools and whether each loads (names and words only, never a
|
|
25
|
+
* secret), or why they couldn't be read. */
|
|
26
|
+
access?: {
|
|
27
|
+
tools: {
|
|
28
|
+
name: string;
|
|
29
|
+
loads: boolean;
|
|
30
|
+
words: string;
|
|
31
|
+
}[];
|
|
32
|
+
} | {
|
|
33
|
+
problem: string;
|
|
34
|
+
};
|
|
23
35
|
};
|
|
24
36
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
25
37
|
export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
|
|
@@ -14,7 +14,34 @@ export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:7
|
|
|
14
14
|
`.project-settings button,.project-delete button{justify-self:start;min-height:36px;white-space:nowrap}.project-delete .actions a{min-height:36px;display:inline-flex;align-items:center}` +
|
|
15
15
|
`.project-settings .builds-at-once{margin-top:20px}.project-settings .builds-at-once h2{font-size:1rem;margin:0 0 4px}.project-settings .builds-at-once form{margin-top:4px;gap:4px}` +
|
|
16
16
|
`.project-settings .builds-at-once .builds-row{display:flex;gap:8px;align-items:stretch}.project-settings .builds-at-once input{width:5.5rem;min-height:44px;margin:0;box-sizing:border-box}.project-settings .builds-at-once button{min-height:44px;margin:0}` +
|
|
17
|
-
`.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}
|
|
17
|
+
`.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}` +
|
|
18
|
+
`.project-settings .build-access{margin-top:20px;min-width:0;overflow-wrap:anywhere}.project-settings .build-access h2{font:var(--so-typography-title-sm);margin:0 0 4px}.project-settings .build-access p{margin:0}` +
|
|
19
|
+
`.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)}` +
|
|
20
|
+
`.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)}` +
|
|
21
|
+
`.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}` +
|
|
22
|
+
`.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}`;
|
|
23
|
+
/** Build access: one sentence for what a build gets, the detail on demand, and the way to change it. */
|
|
24
|
+
function accessHtml(view) {
|
|
25
|
+
const access = view.access;
|
|
26
|
+
if (access === undefined)
|
|
27
|
+
return "";
|
|
28
|
+
const manage = html `<a class="manage-tools" href="/settings/tools?repo=${encodeURIComponent(view.repo)}">Manage tools</a>`;
|
|
29
|
+
const always = [
|
|
30
|
+
"Your other projects, keys and sign-in files are out of reach.",
|
|
31
|
+
"Your own Claude and Codex hooks, plugins and MCP servers don't load.",
|
|
32
|
+
"Builds sign in with your existing Claude or Codex login.",
|
|
33
|
+
];
|
|
34
|
+
if ("problem" in access) {
|
|
35
|
+
return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2><p class="problem" role="alert">${access.problem}</p><details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}</ul></details>${manage}</section>`;
|
|
36
|
+
}
|
|
37
|
+
const loading = access.tools.filter(one => one.loads);
|
|
38
|
+
const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}<span class="tool-name">${one.name}</span>`);
|
|
39
|
+
const outcome = loading.length === 0
|
|
40
|
+
? html `<p>Builds work in their task's folder, with no extra tools.</p>`
|
|
41
|
+
: html `<p>Builds work in their task's folder, with ${loading.length === 1 ? "one tool" : `${loading.length} tools`}: ${names}.</p>`;
|
|
42
|
+
const tools = access.tools.map(one => html `<li><span class="tool-name">${one.name}</span> <span class="meta">· ${one.words}</span></li>`);
|
|
43
|
+
return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2>${outcome}<details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}${tools}</ul></details>${manage}</section>`;
|
|
44
|
+
}
|
|
18
45
|
/** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
|
|
19
46
|
function buildsHtml(view) {
|
|
20
47
|
const b = view.builds;
|
|
@@ -30,7 +57,10 @@ const note = (notice) => notice.problem ? html `<p class="problem" role="alert">
|
|
|
30
57
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
31
58
|
export function projectSettingsHtml(view, notice) {
|
|
32
59
|
const held = html `<p>Toolroll holds ${holdingsWords(view.holdings)} for <strong>${view.name}</strong>.</p>`;
|
|
33
|
-
const
|
|
60
|
+
const remote = html `<section class="build-access recorded-remote" aria-labelledby="recorded-remote-title"><h2 id="recorded-remote-title">Recorded remote</h2>${view.remoteUrl
|
|
61
|
+
? html `<p class="path">${view.remoteUrl}</p>`
|
|
62
|
+
: 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>`;
|
|
63
|
+
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${remote}${buildsHtml(view)}${accessHtml(view)}`;
|
|
34
64
|
if (!view.canDelete)
|
|
35
65
|
return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
|
|
36
66
|
const body = view.running.length > 0
|
package/dist/project-tools.d.ts
CHANGED
|
@@ -142,22 +142,19 @@ export type ToolLaunchArgs = {
|
|
|
142
142
|
cleanup: () => void;
|
|
143
143
|
privateDir?: string;
|
|
144
144
|
};
|
|
145
|
-
/** Claude: only these servers
|
|
145
|
+
/** Claude: only these servers, written to a private 0600 file removed after the run (the provider adds `--strict-mcp-config`). */
|
|
146
146
|
export declare function claudeToolArgs(launch: ToolLaunch): ToolLaunchArgs;
|
|
147
|
-
export declare function carriedCodexSettings(includeModel: boolean, codexHome?: string): string[];
|
|
148
147
|
/** Where the tool launcher lives beside this module, built. */
|
|
149
148
|
export declare function toolLauncherPath(): string;
|
|
150
149
|
/**
|
|
151
|
-
* Codex:
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
150
|
+
* Codex: each tool is named by `-c` and started through the launcher. The
|
|
151
|
+
* operator's config.toml, rules, apps, plugins and hooks are already off
|
|
152
|
+
* (the provider's CODEX_BUILD_ISOLATION_ARGV), and nothing of config.toml
|
|
153
|
+
* is carried. Codex gives its own environment to the agent's shell whatever
|
|
155
154
|
* its policy says, so no secret rides it: each tool's values sit in the
|
|
156
155
|
* run's private 0600 file and reach only that tool's process.
|
|
157
156
|
*/
|
|
158
|
-
export declare function codexToolArgs(launch: ToolLaunch, options
|
|
159
|
-
includeModel: boolean;
|
|
160
|
-
codexHome?: string;
|
|
157
|
+
export declare function codexToolArgs(launch: ToolLaunch, options?: {
|
|
161
158
|
launcher?: string;
|
|
162
159
|
node?: string;
|
|
163
160
|
enabledTools?: Readonly<Record<string, readonly string[]>>;
|
|
@@ -182,12 +179,11 @@ export declare function toolLaunchFor(store: Pick<Store, "projectTools" | "getRu
|
|
|
182
179
|
export declare function prepareRunTools(store: Pick<Store, "projectTools" | "getRun" | "refById" | "getScope" | "toolSealFor" | "recordRunTools" | "orgPolicy">, runId: number, provider: "claude" | "codex" | "openrouter" | "gemini", options: {
|
|
183
180
|
home?: string;
|
|
184
181
|
now: Date;
|
|
185
|
-
includeModel: boolean;
|
|
186
182
|
readOnly?: Readonly<Record<string, readonly string[]>>;
|
|
187
183
|
withheld?: Readonly<Record<string, string>>;
|
|
188
184
|
}): ToolLaunchArgs;
|
|
189
185
|
/** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
|
|
190
|
-
export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini"
|
|
186
|
+
export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini"): ToolLaunchArgs;
|
|
191
187
|
type ToolWrites = Pick<Store, "projectTools" | "addProjectTool" | "removeProjectTool" | "recordProjectToolTest">;
|
|
192
188
|
/** Add one tool (and, for an import, the values it brings) to a project's builds. */
|
|
193
189
|
export declare function addToolTo(store: ToolWrites, repo: string, input: ToolSpec, source: string, by: string, now: Date, options?: {
|
package/dist/project-tools.js
CHANGED
|
@@ -540,7 +540,7 @@ function safeLine(text) {
|
|
|
540
540
|
}
|
|
541
541
|
const toml = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
542
542
|
const tomlList = (values) => `[${values.map(toml).join(",")}]`;
|
|
543
|
-
/** Claude: only these servers
|
|
543
|
+
/** Claude: only these servers, written to a private 0600 file removed after the run (the provider adds `--strict-mcp-config`). */
|
|
544
544
|
export function claudeToolArgs(launch) {
|
|
545
545
|
const servers = {};
|
|
546
546
|
for (const tool of launch.tools) {
|
|
@@ -563,49 +563,27 @@ export function claudeToolArgs(launch) {
|
|
|
563
563
|
const file = join(dir, "mcp.json");
|
|
564
564
|
writeFileSync(file, JSON.stringify({ mcpServers: servers }), { mode: 0o600 });
|
|
565
565
|
return {
|
|
566
|
-
argv: ["--
|
|
566
|
+
argv: ["--mcp-config", file],
|
|
567
567
|
// A first `npx` download can take longer than the default start-up wait.
|
|
568
568
|
env: launch.tools.length > 0 ? { MCP_TIMEOUT: "60000" } : {},
|
|
569
569
|
omitEnv: [],
|
|
570
570
|
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
571
571
|
};
|
|
572
572
|
}
|
|
573
|
-
/** The operator's Codex model settings a build keeps when its tools are replaced: how it thinks, never what it can reach. */
|
|
574
|
-
const CARRIED_CODEX_SETTINGS = ["model_reasoning_effort", "model_reasoning_summary", "model_verbosity", "service_tier", "personality"];
|
|
575
|
-
export function carriedCodexSettings(includeModel, codexHome = process.env["CODEX_HOME"] ?? join(homedir(), ".codex")) {
|
|
576
|
-
let text = "";
|
|
577
|
-
try {
|
|
578
|
-
text = readFileSync(join(codexHome, "config.toml"), "utf8");
|
|
579
|
-
}
|
|
580
|
-
catch {
|
|
581
|
-
return [];
|
|
582
|
-
}
|
|
583
|
-
const wanted = includeModel ? ["model", ...CARRIED_CODEX_SETTINGS] : CARRIED_CODEX_SETTINGS;
|
|
584
|
-
const argv = [];
|
|
585
|
-
for (const line of text.split("\n")) {
|
|
586
|
-
if (/^\s*\[/.test(line))
|
|
587
|
-
break;
|
|
588
|
-
const match = /^\s*([a-z_]+)\s*=\s*("[^"\\\n]{0,80}"|[A-Za-z0-9_.-]{1,40})\s*(?:#.*)?$/.exec(line);
|
|
589
|
-
if (match !== null && wanted.includes(match[1]))
|
|
590
|
-
argv.push("-c", `${match[1]}=${match[2]}`);
|
|
591
|
-
}
|
|
592
|
-
return argv;
|
|
593
|
-
}
|
|
594
573
|
/** Where the tool launcher lives beside this module, built. */
|
|
595
574
|
export function toolLauncherPath() {
|
|
596
575
|
return fileURLToPath(new URL("./tool-launcher.js", import.meta.url));
|
|
597
576
|
}
|
|
598
577
|
/**
|
|
599
|
-
* Codex:
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
578
|
+
* Codex: each tool is named by `-c` and started through the launcher. The
|
|
579
|
+
* operator's config.toml, rules, apps, plugins and hooks are already off
|
|
580
|
+
* (the provider's CODEX_BUILD_ISOLATION_ARGV), and nothing of config.toml
|
|
581
|
+
* is carried. Codex gives its own environment to the agent's shell whatever
|
|
603
582
|
* its policy says, so no secret rides it: each tool's values sit in the
|
|
604
583
|
* run's private 0600 file and reach only that tool's process.
|
|
605
584
|
*/
|
|
606
|
-
export function codexToolArgs(launch, options) {
|
|
607
|
-
|
|
608
|
-
const argv = ["--ignore-user-config", ...carriedCodexSettings(options.includeModel, options.codexHome), "-c", "apps._default.enabled=false", "-c", "features.plugins=false"];
|
|
585
|
+
export function codexToolArgs(launch, options = {}) {
|
|
586
|
+
const argv = [];
|
|
609
587
|
if (launch.tools.length === 0)
|
|
610
588
|
return { argv, env: {}, omitEnv: [], cleanup: () => undefined };
|
|
611
589
|
const dir = mkdtempSync(join(tmpdir(), "so-tools-"));
|
|
@@ -701,9 +679,8 @@ export function prepareRunTools(store, runId, provider, options) {
|
|
|
701
679
|
const launch = limitedTo(toolLaunchFor(store, runId, options.home), options.readOnly, options.withheld);
|
|
702
680
|
const enabledTools = options.readOnly === undefined ? {} : { enabledTools: options.readOnly };
|
|
703
681
|
const prepared = provider === "claude" ? claudeToolArgs(launch)
|
|
704
|
-
: provider === "codex" ? codexToolArgs(launch,
|
|
705
|
-
:
|
|
706
|
-
: { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined };
|
|
682
|
+
: provider === "codex" || provider === "openrouter" ? codexToolArgs(launch, enabledTools)
|
|
683
|
+
: { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined };
|
|
707
684
|
const skipped = provider === "gemini"
|
|
708
685
|
? [...launch.skipped, ...launch.tools.map(one => ({ name: one.spec.name, reason: "Gemini builds can't use project tools yet" }))]
|
|
709
686
|
: launch.skipped;
|
|
@@ -725,13 +702,12 @@ function limitedTo(launch, readOnly, withheld = {}) {
|
|
|
725
702
|
};
|
|
726
703
|
}
|
|
727
704
|
/** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
|
|
728
|
-
export function noToolsArgs(provider
|
|
729
|
-
const none = { tools: [], skipped: [] };
|
|
705
|
+
export function noToolsArgs(provider) {
|
|
730
706
|
if (provider === "claude")
|
|
731
|
-
return { argv: ["--
|
|
707
|
+
return { argv: ["--mcp-config", '{"mcpServers":{}}'], env: {}, omitEnv: [], cleanup: () => undefined };
|
|
732
708
|
if (provider === "gemini")
|
|
733
709
|
return { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined };
|
|
734
|
-
return codexToolArgs(
|
|
710
|
+
return codexToolArgs({ tools: [], skipped: [] });
|
|
735
711
|
}
|
|
736
712
|
/** Add one tool (and, for an import, the values it brings) to a project's builds. */
|
|
737
713
|
export function addToolTo(store, repo, input, source, by, now, options = {}) {
|
package/dist/provider.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export type Invocation = {
|
|
|
64
64
|
* Codex renders it as its own sandbox profile; Claude as deny rules for
|
|
65
65
|
* its file tools (its shell is fenced by the macOS sandbox around it). */
|
|
66
66
|
fence?: readonly string[];
|
|
67
|
+
/** More paths Claude's own file tools refuse, beside the fence: home
|
|
68
|
+
* configuration its CLI still reads itself (so the sandbox around the
|
|
69
|
+
* whole process can't deny them). */
|
|
70
|
+
fileToolFence?: readonly string[];
|
|
71
|
+
/** The task's own folder (its worktree). A Claude build loads no settings
|
|
72
|
+
* files, so this is where the repository's CLAUDE.md is read from. */
|
|
73
|
+
workspace?: string;
|
|
67
74
|
/** A running build's steering folder (steer-mailbox.ts): the absolute path
|
|
68
75
|
* and the worktree-relative name its brief uses. Claude renders a
|
|
69
76
|
* PostToolUse hook that says when a note is waiting; every other provider
|
|
@@ -72,6 +79,11 @@ export type Invocation = {
|
|
|
72
79
|
dir: string;
|
|
73
80
|
name: string;
|
|
74
81
|
};
|
|
82
|
+
/** Live input (live-input.ts): the brief arrives as the first stdin message
|
|
83
|
+
* and stdin stays open, so the argv carries `--input-format stream-json`
|
|
84
|
+
* and never the brief (Claude drops a `-p` prompt once stdin carries
|
|
85
|
+
* input). Set by the gateway only, for a transport that streams input. */
|
|
86
|
+
streamInput?: boolean;
|
|
75
87
|
/** Claude's structured-output floor for a non-review phase: the scout's
|
|
76
88
|
* report rides the terminal result event instead of a file plan mode
|
|
77
89
|
* would refuse to write. Ignored by providers without `--json-schema`. */
|
|
@@ -115,6 +127,9 @@ export type ParsedEnvelope = {
|
|
|
115
127
|
tokensIn: number | null;
|
|
116
128
|
tokensOut: number | null;
|
|
117
129
|
costUsd: number | null;
|
|
130
|
+
/** The run spent but its cost could not be read (Claude's main result was too large to keep): the gateway charges
|
|
131
|
+
* the run's dollar cap, or records it unmeasured when none applied. Absent = the cost is what costUsd says. */
|
|
132
|
+
costUnknown?: boolean;
|
|
118
133
|
usageRaw: string | null;
|
|
119
134
|
/**
|
|
120
135
|
* Whether the provider was seen to initialize (codex: thread.started;
|
|
@@ -152,6 +167,9 @@ export type ParsedEnvelope = {
|
|
|
152
167
|
type Adapter = {
|
|
153
168
|
binary: string;
|
|
154
169
|
argv(invocation: Invocation): string[];
|
|
170
|
+
/** Environment the launch needs beside the argv (Claude: read the
|
|
171
|
+
* workspace's CLAUDE.md through `--add-dir`). */
|
|
172
|
+
env?(invocation: Invocation): Record<string, string>;
|
|
155
173
|
/** Prompt input for transports that use stdin instead of a bounded OS argument. */
|
|
156
174
|
stdin?(invocation: Invocation): string | undefined;
|
|
157
175
|
parse(stdout: string): ParsedEnvelope;
|
|
@@ -167,8 +185,47 @@ type Adapter = {
|
|
|
167
185
|
clampTimeout(phase: Phase, requestedMs: number): number;
|
|
168
186
|
};
|
|
169
187
|
export declare const OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
|
|
188
|
+
/**
|
|
189
|
+
* A build's own setup (plan, build and repair; never a review, which keeps
|
|
190
|
+
* the isolation above). No settings file loads — not the user's, the
|
|
191
|
+
* repository's or the local one — so none of their hooks, plugins or
|
|
192
|
+
* permissions reach the run; Toolroll's one `--settings` (the fence and the
|
|
193
|
+
* steering hook) is all there is. MCP servers come only from `--mcp-config`:
|
|
194
|
+
* the project's named tools, or an explicit empty list. The repository's
|
|
195
|
+
* CLAUDE.md still loads, through `--add-dir` on the workspace. Sign-in is
|
|
196
|
+
* the CLI's own (keychain or ~/.claude), which no setting source governs.
|
|
197
|
+
* `-p` never shows the workspace trust prompt (Claude Code 2.1.290), so a
|
|
198
|
+
* fresh worktree can't wedge a headless run. Checked against 2.1.290 with
|
|
199
|
+
* planted user, project and local hooks and MCP servers: none loaded.
|
|
200
|
+
*/
|
|
201
|
+
export declare const CLAUDE_BUILD_ISOLATION_ARGV: readonly string[];
|
|
170
202
|
/** Claude's own tools that only make sense with a person to wake them. */
|
|
171
203
|
export declare const HEADLESS_DISALLOWED_TOOLS: readonly string[];
|
|
204
|
+
/**
|
|
205
|
+
* Whether a failed launch was an installed CLI too old for a build's own
|
|
206
|
+
* setup: it names one of those flags as unknown. Then nothing ran, and the
|
|
207
|
+
* run says so plainly instead of reading as a failed sign-in or bad work.
|
|
208
|
+
*/
|
|
209
|
+
export declare function isolationUnsupported(provider: ProviderId, stderr: string): string | null;
|
|
210
|
+
/**
|
|
211
|
+
* Codex's own fail-closed equivalent of `CLAUDE_REVIEW_ISOLATION_ARGV`:
|
|
212
|
+
* read-only sandbox rather than the ordinary `workspace-write` (a reviewer
|
|
213
|
+
* dispatched to codex was, until this, handed a write-capable sandbox —
|
|
214
|
+
* the same class of leak the claude side had), approvals that REFUSE
|
|
215
|
+
* rather than wait on a prompt a headless turn can never answer, and the
|
|
216
|
+
* user's own `~/.codex/config.toml` — which can name arbitrary MCP
|
|
217
|
+
* servers, exactly the surface `--strict-mcp-config` closes for claude —
|
|
218
|
+
* ignored outright.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* A Codex build's own setup, the equivalent of Claude's: the operator's
|
|
222
|
+
* config.toml (its MCP servers, model choices, profiles) and execpolicy
|
|
223
|
+
* rules ignored, ChatGPT apps and plugins off (plugins carry their own MCP
|
|
224
|
+
* servers), and hooks off. Sign-in still comes from CODEX_HOME, which
|
|
225
|
+
* `--ignore-user-config` leaves alone. The project's named tools follow as
|
|
226
|
+
* `-c mcp_servers.*` (project-tools.ts).
|
|
227
|
+
*/
|
|
228
|
+
export declare const CODEX_BUILD_ISOLATION_ARGV: readonly string[];
|
|
172
229
|
/**
|
|
173
230
|
* The diagnostic discipline (Phase 3 B6/C6): the harness's own words are
|
|
174
231
|
* untrusted bytes headed for refusal screens and notifications — controls
|
|
@@ -188,11 +245,10 @@ export declare const ALL_CREDENTIAL_ENV: readonly string[];
|
|
|
188
245
|
export declare function adapterFor(provider: ProviderId): Adapter;
|
|
189
246
|
/**
|
|
190
247
|
* The provider audit: what each harness supports, what we actually pass,
|
|
191
|
-
* and what user-global configuration
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* instead of hope. `enforced` is a literal false until that day.
|
|
248
|
+
* and what user-global configuration still reaches an unattended run.
|
|
249
|
+
* None of this changes an invocation — it states facts an operator reads
|
|
250
|
+
* on `providers`. Claude and Codex builds now run in their own setup
|
|
251
|
+
* (`enforced`); Gemini's flag is still only reported.
|
|
196
252
|
*/
|
|
197
253
|
export type ProviderAudit = {
|
|
198
254
|
/** How output reaches us — and therefore which signals can exist at all. */
|
|
@@ -217,13 +273,14 @@ export type ProviderAudit = {
|
|
|
217
273
|
*/
|
|
218
274
|
terminalContract: "required" | "none";
|
|
219
275
|
isolation: {
|
|
220
|
-
/** The harness's hermetic flag, if it has one.
|
|
276
|
+
/** The harness's hermetic flag, if it has one. */
|
|
221
277
|
flag: string | null;
|
|
222
278
|
/** Whether that flag preserves resume. false = documented conflict. */
|
|
223
279
|
resumeSafe: boolean | null;
|
|
224
|
-
|
|
280
|
+
/** Whether every build, fresh or resumed, passes it. */
|
|
281
|
+
enforced: boolean;
|
|
225
282
|
};
|
|
226
|
-
/**
|
|
283
|
+
/** What still reaches a build: its own sources and the user surfaces left. */
|
|
227
284
|
configSurface: readonly string[];
|
|
228
285
|
};
|
|
229
286
|
/** Read-only facts about a provider — safe anywhere, spawns nothing. */
|