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.
Files changed (128) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/agent-board.d.ts +208 -0
  3. package/dist/agent-board.js +454 -0
  4. package/dist/agent-fence.d.ts +19 -0
  5. package/dist/agent-fence.js +100 -8
  6. package/dist/agent-pulse.d.ts +43 -0
  7. package/dist/agent-pulse.js +214 -0
  8. package/dist/agent-turns.d.ts +52 -0
  9. package/dist/agent-turns.js +137 -0
  10. package/dist/agents-board.d.ts +101 -0
  11. package/dist/agents-board.js +225 -0
  12. package/dist/assignment-adapters.d.ts +1 -0
  13. package/dist/assignment-status.d.ts +1 -1
  14. package/dist/assignment.d.ts +4 -0
  15. package/dist/assignment.js +8 -6
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +29 -25
  18. package/dist/browser-crew.js +2 -0
  19. package/dist/browser-workspace.d.ts +79 -1
  20. package/dist/browser-workspace.js +1 -0
  21. package/dist/builder.d.ts +8 -0
  22. package/dist/builder.js +113 -13
  23. package/dist/chat-channel.js +9 -0
  24. package/dist/chat-core.d.ts +6 -0
  25. package/dist/chat-core.js +8 -0
  26. package/dist/chat-delivery.js +22 -0
  27. package/dist/chat-messages.js +8 -1
  28. package/dist/chat-quiet.js +1 -0
  29. package/dist/chat-voice.d.ts +2 -0
  30. package/dist/chat-voice.js +2 -0
  31. package/dist/checkout-cleanup.d.ts +3 -0
  32. package/dist/checkout-cleanup.js +11 -2
  33. package/dist/cli.js +28 -11
  34. package/dist/contracts/console-api.d.ts +3 -1
  35. package/dist/contracts/console-api.js +5 -1
  36. package/dist/contracts/lead-tools.d.ts +16 -2
  37. package/dist/contracts/lead-tools.js +5 -1
  38. package/dist/desktop-update.d.ts +1 -1
  39. package/dist/evidence-pack.js +1 -1
  40. package/dist/evidence.d.ts +6 -0
  41. package/dist/evidence.js +14 -3
  42. package/dist/exec.d.ts +32 -0
  43. package/dist/exec.js +229 -10
  44. package/dist/invoke.js +110 -18
  45. package/dist/landed-cleanup.d.ts +170 -0
  46. package/dist/landed-cleanup.js +814 -0
  47. package/dist/lead-cli.js +22 -19
  48. package/dist/lead-contract.d.ts +1 -1
  49. package/dist/lead-contract.js +4 -4
  50. package/dist/lead-doors.js +45 -1
  51. package/dist/lead-tools.js +40 -7
  52. package/dist/live-input.d.ts +68 -0
  53. package/dist/live-input.js +254 -0
  54. package/dist/live.d.ts +1 -1
  55. package/dist/live.js +4 -2
  56. package/dist/maintenance.d.ts +2 -0
  57. package/dist/maintenance.js +5 -2
  58. package/dist/operate-remote-arguments.js +5 -1
  59. package/dist/operate-remote.js +1 -1
  60. package/dist/operate.d.ts +1 -1
  61. package/dist/operate.js +249 -26
  62. package/dist/orphan-sweep.d.ts +2 -0
  63. package/dist/orphan-sweep.js +5 -3
  64. package/dist/project-admission.d.ts +10 -6
  65. package/dist/project-admission.js +33 -11
  66. package/dist/project-delete-ui.d.ts +12 -0
  67. package/dist/project-delete-ui.js +32 -2
  68. package/dist/project-tools.d.ts +7 -11
  69. package/dist/project-tools.js +13 -37
  70. package/dist/provider.d.ts +65 -8
  71. package/dist/provider.js +80 -21
  72. package/dist/remote-main.d.ts +57 -0
  73. package/dist/remote-main.js +179 -0
  74. package/dist/result-completion.d.ts +7 -0
  75. package/dist/result-completion.js +9 -0
  76. package/dist/runner.d.ts +4 -2
  77. package/dist/runner.js +3 -3
  78. package/dist/serve.js +19 -1
  79. package/dist/server/live.d.ts +3 -0
  80. package/dist/server/live.js +35 -2
  81. package/dist/server/pages.js +59 -35
  82. package/dist/server/render-chat.js +12 -0
  83. package/dist/server/render-tasks.d.ts +15 -2
  84. package/dist/server/render-tasks.js +30 -7
  85. package/dist/server/route-table.js +4 -1
  86. package/dist/server/settings.js +5 -3
  87. package/dist/server/task-views.js +11 -0
  88. package/dist/server/tasks.js +63 -0
  89. package/dist/shared-deps.d.ts +9 -4
  90. package/dist/shared-deps.js +89 -13
  91. package/dist/staged-runtimes.d.ts +16 -4
  92. package/dist/staged-runtimes.js +160 -38
  93. package/dist/steer-live.d.ts +19 -0
  94. package/dist/steer-live.js +93 -0
  95. package/dist/steer-status.d.ts +8 -2
  96. package/dist/steer-status.js +19 -5
  97. package/dist/storage-sweep-child.d.ts +1 -0
  98. package/dist/storage-sweep-child.js +76 -0
  99. package/dist/storage-sweep-process.d.ts +48 -0
  100. package/dist/storage-sweep-process.js +204 -0
  101. package/dist/storage-sweep-schedule.d.ts +30 -0
  102. package/dist/storage-sweep-schedule.js +164 -0
  103. package/dist/storage-sweep.d.ts +31 -6
  104. package/dist/storage-sweep.js +165 -39
  105. package/dist/storage-ui.d.ts +4 -0
  106. package/dist/storage-ui.js +22 -7
  107. package/dist/storage.d.ts +6 -0
  108. package/dist/storage.js +35 -0
  109. package/dist/store.d.ts +200 -3
  110. package/dist/store.js +443 -17
  111. package/dist/surface.js +8 -4
  112. package/dist/sweep-quarantine.d.ts +28 -0
  113. package/dist/sweep-quarantine.js +153 -0
  114. package/dist/task-base.d.ts +35 -0
  115. package/dist/task-base.js +51 -0
  116. package/dist/task-status.d.ts +2 -0
  117. package/dist/task-status.js +5 -1
  118. package/dist/test-temp.d.ts +6 -3
  119. package/dist/test-temp.js +15 -9
  120. package/dist/toolroll-update.js +4 -2
  121. package/dist/work-index.d.ts +1 -0
  122. package/dist/work-index.js +32 -12
  123. package/dist/work-summary.d.ts +2 -2
  124. package/dist/workspace-revision.js +3 -1
  125. package/dist/worktree.d.ts +57 -3
  126. package/dist/worktree.js +201 -23
  127. package/package.json +1 -1
  128. package/scripts/provider-canary.mjs +235 -147
@@ -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 roots = sweepRoots(store, options.pool ?? null, census, options.tempRoots ?? [], now());
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, roots, freshGuard).filter(one => {
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
- * Admit one project. Idempotent: adding a saved project again changes
37
- * nothing and writes no ledger row. The registry is written first, under
38
- * its lock; the project row and its ledger row then commit together, so a
39
- * retry after a failure finishes the job and records it exactly once.
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.upsertProject(repo, projectName(repo), input.now);
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 head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${buildsHtml(view)}`;
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 &lt;project&gt; --remote &lt;url&gt;</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
@@ -142,22 +142,19 @@ export type ToolLaunchArgs = {
142
142
  cleanup: () => void;
143
143
  privateDir?: string;
144
144
  };
145
- /** Claude: only these servers (`--strict-mcp-config`), written to a private 0600 file removed after the run. */
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: the operator's config.toml is ignored whole (its MCP servers
152
- * with it), ChatGPT apps and plugins are off, the model settings above
153
- * are carried, and each tool is named by `-c` and started through the
154
- * launcher. Codex gives its own environment to the agent's shell whatever
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", includeModel: boolean): ToolLaunchArgs;
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?: {
@@ -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 (`--strict-mcp-config`), written to a private 0600 file removed after the run. */
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: ["--strict-mcp-config", "--mcp-config", file],
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: the operator's config.toml is ignored whole (its MCP servers
600
- * with it), ChatGPT apps and plugins are off, the model settings above
601
- * are carried, and each tool is named by `-c` and started through the
602
- * launcher. Codex gives its own environment to the agent's shell whatever
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
- // ChatGPT apps, connectors and plugins are the operator's account, not the project's: all off (plugins carry their own MCP servers).
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, { includeModel: options.includeModel, ...enabledTools })
705
- : provider === "openrouter" ? codexToolArgs(launch, { includeModel: false, ...enabledTools })
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, includeModel) {
729
- const none = { tools: [], skipped: [] };
705
+ export function noToolsArgs(provider) {
730
706
  if (provider === "claude")
731
- return { argv: ["--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}'], env: {}, omitEnv: [], cleanup: () => undefined };
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(none, { includeModel: provider === "codex" && includeModel });
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 = {}) {
@@ -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 can leak into an unattended run.
192
- * REPORT BEFORE ENFORCEMENT (Codex roadmap review, item 10): none of this
193
- * changes an invocation — it states facts an operator reads on `providers`,
194
- * so hermetic mode can later be turned on per provider from evidence
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. We do not pass it. */
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
- enforced: false;
280
+ /** Whether every build, fresh or resumed, passes it. */
281
+ enforced: boolean;
225
282
  };
226
- /** User-global surfaces that can reach an invocation today. */
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. */