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.
Files changed (78) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +43 -17
  3. package/THIRD_PARTY_NOTICES.md +21 -1
  4. package/dist/agent-fence.d.ts +204 -81
  5. package/dist/agent-fence.js +467 -397
  6. package/dist/agentconfig.d.ts +1 -1
  7. package/dist/agentconfig.js +11 -2
  8. package/dist/batch-checks.js +14 -26
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +21 -0
  10. package/dist/builder.d.ts +9 -0
  11. package/dist/builder.js +50 -4
  12. package/dist/check-access.d.ts +51 -0
  13. package/dist/check-access.js +77 -0
  14. package/dist/coding-provider.d.ts +14 -0
  15. package/dist/coding-provider.js +33 -1
  16. package/dist/coding-workspace.d.ts +15 -0
  17. package/dist/coding-workspace.js +9 -2
  18. package/dist/contracts/console-api.d.ts +2 -0
  19. package/dist/contracts/console-api.js +2 -0
  20. package/dist/doctor.d.ts +7 -0
  21. package/dist/doctor.js +4 -0
  22. package/dist/egress.d.ts +2 -0
  23. package/dist/egress.js +13 -4
  24. package/dist/exec.d.ts +17 -3
  25. package/dist/exec.js +54 -17
  26. package/dist/flow-actions.d.ts +1 -0
  27. package/dist/flow-actions.js +3 -2
  28. package/dist/flow-code.d.ts +31 -2
  29. package/dist/flow-code.js +57 -5
  30. package/dist/flow-steps.d.ts +4 -0
  31. package/dist/flow-steps.js +9 -5
  32. package/dist/flow-triggers.d.ts +4 -0
  33. package/dist/flow-triggers.js +8 -3
  34. package/dist/invoke.d.ts +50 -3
  35. package/dist/invoke.js +204 -66
  36. package/dist/operate.d.ts +4 -0
  37. package/dist/operate.js +119 -0
  38. package/dist/project-delete-ui.d.ts +23 -6
  39. package/dist/project-delete-ui.js +94 -1
  40. package/dist/project-sandbox.d.ts +59 -0
  41. package/dist/project-sandbox.js +83 -0
  42. package/dist/project-tools.d.ts +29 -5
  43. package/dist/project-tools.js +43 -7
  44. package/dist/release.d.ts +1 -1
  45. package/dist/release.js +1 -0
  46. package/dist/result-follow-ups.js +10 -15
  47. package/dist/sandbox-launch.d.ts +1 -0
  48. package/dist/sandbox-launch.js +148 -0
  49. package/dist/sandbox-runtime.d.ts +62 -0
  50. package/dist/sandbox-runtime.js +140 -0
  51. package/dist/scope.d.ts +37 -0
  52. package/dist/scope.js +32 -5
  53. package/dist/scout-browser-lock.d.ts +69 -0
  54. package/dist/scout-browser-lock.js +69 -0
  55. package/dist/scout-browser.d.ts +60 -1
  56. package/dist/scout-browser.js +200 -4
  57. package/dist/scout.d.ts +9 -4
  58. package/dist/scout.js +14 -4
  59. package/dist/serve.js +12 -1
  60. package/dist/server/http.d.ts +2 -0
  61. package/dist/server/render-results.d.ts +3 -0
  62. package/dist/server/render-results.js +1 -0
  63. package/dist/server/render-tasks.js +5 -1
  64. package/dist/server/route-table.js +2 -0
  65. package/dist/server/settings.js +64 -0
  66. package/dist/server/tasks.js +2 -0
  67. package/dist/store.d.ts +59 -2
  68. package/dist/store.js +166 -4
  69. package/dist/subagent-tools.d.ts +1 -0
  70. package/dist/subagent-tools.js +7 -5
  71. package/dist/surface.js +2 -1
  72. package/dist/tool-broker.d.ts +2 -1
  73. package/dist/tool-broker.js +2 -2
  74. package/dist/tool-launcher.js +10 -16
  75. package/package.json +2 -1
  76. package/scripts/provider-canary.mjs +5 -6
  77. package/dist/egress-bridge.d.ts +0 -17
  78. package/dist/egress-bridge.js +0 -111
package/dist/serve.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { taskMovedSince } from "./chat-ask.js";
2
+ import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
3
+ import { signInsUnavailableWords } from "./check-access.js";
2
4
  import { LIVE_VIEW_CONSENT, liveCommandsShown, liveViewConsentDigest } from "./live-detail.js";
3
5
  import { html, htmlString, jsonScript, textHtml } from "./html.js";
4
6
  import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
@@ -14,6 +16,7 @@ import { boundProjectSummary, browserCrewFromIndex, browserCrewOf, browserNaviga
14
16
  import { sharedActionPayload } from './chat-actions.js';
15
17
  import { prepareCodingContext } from './coding-context.js';
16
18
  import { CodingWorkspace } from './coding-workspace.js';
19
+ import { codingSessionPermissions } from './coding-provider.js';
17
20
  import { CONSOLE_FORMS, readForm } from "./contracts/console-api.js";
18
21
  import { createFlowRooms, flowFingerprint } from "./flow-live.js";
19
22
  import {} from "./google-mail.js";
@@ -193,7 +196,7 @@ export function createDecisionServer(options) {
193
196
  const database = store.handle.prepare('PRAGMA database_list').all().find(row => row['name'] === 'main')?.['file'];
194
197
  if (typeof database === 'string' && database !== '') {
195
198
  try {
196
- coding = new CodingWorkspace({ database: `${database}.coding.sqlite`, worktreeRoot: join(dirname(database), 'coding-worktrees'), admissionPaused: () => updateAdmissionPaused(store.raw()), authorize: (actor, repo) => codingActorAllowed(actor) && codingProjectAllowed(repo), context: input => prepareCodingContext(store, { ...input, root: join(dirname(database), 'coding-context') }) });
199
+ coding = new CodingWorkspace({ database: `${database}.coding.sqlite`, worktreeRoot: join(dirname(database), 'coding-worktrees'), permissions: codingSessionPermissions(store, join(dirname(database), 'coding-worktrees')), admissionPaused: () => updateAdmissionPaused(store.raw()), authorize: (actor, repo) => codingActorAllowed(actor) && codingProjectAllowed(repo), context: input => prepareCodingContext(store, { ...input, root: join(dirname(database), 'coding-context') }) });
197
200
  }
198
201
  catch (error) {
199
202
  codingProblem = error instanceof Error ? error.message : 'The coding workspace could not open.';
@@ -448,6 +451,14 @@ export function createDecisionServer(options) {
448
451
  access: projectAccessOf(repo),
449
452
  network: { setting: store.projectNetwork(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
450
453
  writes: { setting: store.projectWrites(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
454
+ sandbox: { setting: store.projectSandbox(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
455
+ checkAccess: {
456
+ setting: store.projectCheckAccess(repo), changed: store.projectCheckAccessChange(repo),
457
+ canNarrow: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo),
458
+ canWiden: who.via === "cookie" && !store.isDemo() && store.isInstanceOperator(who.name),
459
+ unconfinedHere: !(options.fenceAvailable?.() ?? (macosFenceAvailable() || linuxFenceAvailable())) && store.unfencedAgentsAllowed(),
460
+ signInsUnavailable: signInsUnavailableWords(options.platform ?? process.platform),
461
+ },
451
462
  liveView: (() => {
452
463
  const commands = liveCommandsShown(store, repo);
453
464
  const canChange = who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo);
@@ -62,6 +62,8 @@ export type ServeOptions = {
62
62
  leadByDefault?: boolean;
63
63
  /** Test seam: the operating system the sign-in command is written for. */
64
64
  platform?: NodeJS.Platform;
65
+ /** Test seam: whether this machine has an OS fence (macOS sandbox or bubblewrap) for checks. */
66
+ fenceAvailable?: () => boolean;
65
67
  /** Extra Host values this server answers as (a Tailscale name, a LAN ip:port). */
66
68
  allowedHosts?: readonly string[];
67
69
  /**
@@ -1,4 +1,5 @@
1
1
  /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
+ import type { CheckAccess } from "../check-access.js";
2
3
  import type { RunNetworkRecord } from "../egress.js";
3
4
  import { type AssignmentSnapshot } from "../assignment.js";
4
5
  import { type BrowserCheckItem, type BrowserNeedAction, type BrowserResultPanel, type BrowserResultView } from "../browser-workspace.js";
@@ -184,6 +185,8 @@ export type ReviewCockpitView = {
184
185
  efficiency?: RunEfficiency | null;
185
186
  /** What the run's network was and what its proxy saw (egress.ts): display only. */
186
187
  network?: RunNetworkRecord | null;
188
+ /** How far its checks reached (check-access.ts): shown only when wider than sandboxed. */
189
+ checkAccess?: CheckAccess;
187
190
  taskId: string;
188
191
  title: string;
189
192
  repo: string | null;
@@ -629,6 +629,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
629
629
  record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
630
630
  { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
631
631
  { label: "Worker", value: run.runner },
632
+ ...(view.checkAccess === undefined || view.checkAccess === "sandboxed" ? [] : [{ label: "Checks", value: view.checkAccess === "unsandboxed" ? "Checks ran without a sandbox" : "Checks ran with model and GitHub sign-ins" }]),
632
633
  ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
633
634
  ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
634
635
  ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
@@ -1,6 +1,8 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
2
  import { networkWords } from "../egress.js";
3
3
  import { writesWords } from "../writes-mode.js";
4
+ import { checkAccessWords } from "../check-access.js";
5
+ import { sandboxTermsWords } from "../project-sandbox.js";
4
6
  import { ACP_AGENTS, isAcpProfile } from "../acp-agents.js";
5
7
  import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
6
8
  import { closedQuestionReason, closedQuestionWords } from "../decision.js";
@@ -541,7 +543,9 @@ export function profileWords(scope) {
541
543
  ? html `<p class="meta">Runs on <span class="mono">${ACP_AGENTS[profile.provider].name} (via ACP) · ${profile.model}</span> — full access inside the OS fence; the agent runs any tool it chooses and nothing asks. No turn limit: stops after ${Math.round(profile.timeoutSeconds / 60)} min, or ${Math.round(profile.idleTimeoutSeconds / 60)} min silent; repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min, or ${Math.round(profile.repairIdleTimeoutSeconds / 60)} min silent.</p>`
542
544
  : html `<p class="meta">Runs on <span class="mono">${profile.provider} · ${profile.model}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
543
545
  // What the build may reach is part of what is approved (egress.ts): said on every approval form.
544
- return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>`;
546
+ // Its sandbox terms (project-sandbox.ts): what it reads always, and any widening, part of what the yes agrees to.
547
+ const terms = sandboxTermsWords(profile);
548
+ return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>${profile.checkAccess === undefined ? "" : html `<p class="meta" data-scope-checks>Checks: ${checkAccessWords(profile.checkAccess)}.</p>`}<p class="meta" data-scope-reads>Reads: ${terms.reads}.</p>${terms.mach === null ? "" : html `<p class="meta" data-scope-mach>${terms.mach.charAt(0).toUpperCase()}${terms.mach.slice(1)}.</p>`}`;
545
549
  }
546
550
  export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
547
551
  export function agentsStandingWords(view) {
@@ -284,6 +284,8 @@ post("settings", "settings.project-concurrency", "/settings/project/concurrency"
284
284
  post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
285
285
  post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
286
286
  post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
287
+ post("settings", "settings.project-check-access", "/settings/project/check-access", { scope: "step-up", callers: ["cookie"] });
288
+ post("settings", "settings.project-sandbox", "/settings/project/sandbox", { scope: "step-up", callers: ["cookie"] });
287
289
  post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
288
290
  // Lead sweeps (docs/plans/lead-fan-out.md): the table, and cancelling what isn't ready. The handler shows a sweep only in
289
291
  // a project the reader can see, and acts only for an approver.
@@ -81,6 +81,8 @@ import { checkPolicy, parseList, policyParts } from "../policy.js";
81
81
  import { parseProjectConcurrency, saveProjectConcurrency } from "../project-concurrency.js";
82
82
  import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
83
83
  import { isWritesMode } from "../writes-mode.js";
84
+ import { isCheckAccess, signInsUnavailableWords, widerThan } from "../check-access.js";
85
+ import { parseMachLookup, parseReadDeny } from "../project-sandbox.js";
84
86
  import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
85
87
  import { deleteProject, holdingsWords } from "../project-delete.js";
86
88
  import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
@@ -2485,6 +2487,8 @@ ${modeWords(terms)
2485
2487
  "settings.project-live-view": projectLiveView,
2486
2488
  "settings.project-network": projectNetwork,
2487
2489
  "settings.project-writes": projectWrites,
2490
+ "settings.project-check-access": projectCheckAccess,
2491
+ "settings.project-sandbox": projectSandbox,
2488
2492
  "settings.project-delete": projectDelete,
2489
2493
  "settings.policy-send": policySave,
2490
2494
  "settings.approval-send": approvalSave,
@@ -2587,5 +2591,65 @@ ${modeWords(terms)
2587
2591
  const changed = store.setProjectWrites(repo, wanted, who.name, now);
2588
2592
  return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2589
2593
  }
2594
+ // Settings → Project → Check access (check-access.ts). Widening is an instance operator's act with a password typed now
2595
+ // (an identity-provider re-check doesn't count); any approver for the project narrows it. The level is compared again
2596
+ // inside the write, so a change made meanwhile can't turn a narrowing into a widening. The ledger keeps before → after.
2597
+ async function projectCheckAccess(ctx) {
2598
+ const { who, response, now, posted } = ctx;
2599
+ const body = readForm(posted, CONSOLE_FORMS.projectCheckAccess);
2600
+ const repo = body.get("repo") ?? "";
2601
+ if (!consoleProjects().includes(repo) || !visible(repo))
2602
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
2603
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2604
+ return refuse(response, who, 403, "An approver for this project changes how far its checks reach.", "/settings/project");
2605
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#check-access");
2606
+ const wanted = body.get("level");
2607
+ if (!isCheckAccess(wanted))
2608
+ return back("problem", "Choose how far checks reach. Nothing changed.");
2609
+ // Not available yet (check-access.ts signInsAvailable): refused before the password, whatever the form sent.
2610
+ const unavailable = wanted === "sign-ins" ? signInsUnavailableWords(options.platform ?? process.platform) : null;
2611
+ if (unavailable !== null)
2612
+ return back("problem", `${unavailable} Nothing changed.`);
2613
+ const widening = widerThan(wanted, store.projectCheckAccess(repo));
2614
+ // Sign-ins with an open network: a second, explicit yes, said before the password.
2615
+ if (wanted === "sign-ins" && widening && store.projectNetwork(repo).mode === "open" && body.get("confirm-open") !== "yes") {
2616
+ return back("problem", "This project's network is open: confirm that its checks could send your sign-ins to any host. Nothing changed.");
2617
+ }
2618
+ const proved = authenticateCredential(who, body.get("password") ?? "");
2619
+ if (!proved.ok)
2620
+ return back("problem", "Enter your Toolroll password to change how far checks reach.");
2621
+ if (widening && (proved.method !== "password-now" || !store.isInstanceOperator(who.name))) {
2622
+ return back("problem", "Only an instance operator widens how far checks reach, with their password typed now. Nothing changed.");
2623
+ }
2624
+ const changed = store.setProjectCheckAccess(repo, wanted, who.name, now, { may: widening ? "widen" : "narrow" });
2625
+ if (changed === null)
2626
+ return back("problem", "Check access changed meanwhile, and this would now widen it. Nothing changed.");
2627
+ return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2628
+ }
2629
+ // Settings → Project → Home folder (project-sandbox.ts, sandbox parity C9): what the project's builds and checks may not
2630
+ // read, and the macOS services they may also use. An approver for the project changes it with
2631
+ // their password typed now; the ledger keeps before → after. New scopes seal it; approved ones keep theirs.
2632
+ async function projectSandbox(ctx) {
2633
+ const { who, response, now, posted } = ctx;
2634
+ const body = readForm(posted, CONSOLE_FORMS.projectSandbox);
2635
+ const repo = body.get("repo") ?? "";
2636
+ if (!consoleProjects().includes(repo) || !visible(repo))
2637
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
2638
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2639
+ return refuse(response, who, 403, "An approver for this project changes its sandbox.", "/settings/project");
2640
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#sandbox");
2641
+ const lines = (name) => (body.get(name) ?? "").split(/\r?\n/);
2642
+ const parsed = { readDeny: parseReadDeny(lines("read-deny")), machLookup: parseMachLookup(lines("mach")) };
2643
+ for (const one of Object.values(parsed))
2644
+ if (!one.ok)
2645
+ return back("problem", `${one.problem.charAt(0).toUpperCase()}${one.problem.slice(1)}. Nothing changed.`);
2646
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
2647
+ return back("problem", "Enter your Toolroll password to change the project's sandbox.");
2648
+ const wanted = { readDeny: parsed.readDeny.ok ? parsed.readDeny.list : [], machLookup: parsed.machLookup.ok ? parsed.machLookup.list : [] };
2649
+ const changed = store.setProjectSandbox(repo, wanted, who.name, now);
2650
+ if (changed === null)
2651
+ return back("problem", "An approver for this project changes its sandbox. Nothing changed.");
2652
+ return back("said", JSON.stringify(changed.before) === JSON.stringify(changed.after) ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2653
+ }
2590
2654
  return { registrations, modelsView };
2591
2655
  }
@@ -2846,6 +2846,8 @@ export function createTasksHandlers(runtime) {
2846
2846
  } })(),
2847
2847
  // What its network was and what its proxy saw (egress.ts), display only.
2848
2848
  network: store.runNetwork(run.id),
2849
+ // How far its checks reached (check-access.ts), display only.
2850
+ checkAccess: store.runCheckAccess(run.id),
2849
2851
  reviewRetry: store.reviewRetryStateOf(run.id),
2850
2852
  review: reviewFactsFor(run.id),
2851
2853
  notes: store.notesForRun(run.id),
package/dist/store.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { type WritesMode } from "./writes-mode.js";
2
- import { type ProjectNetwork, type RunNetworkRecord } from "./egress.js";
2
+ import { type CheckAccess } from "./check-access.js";
3
+ import { type ProjectSandbox } from "./project-sandbox.js";
4
+ import type { FencePolicy } from "./agent-fence.js";
5
+ import { type NetworkProfile, type ProjectNetwork, type RunNetworkRecord } from "./egress.js";
3
6
  import { type AcpLaunch } from "./acp-attest.js";
4
7
  import { type ChatBinding } from "./chat-core.js";
5
8
  import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
@@ -179,7 +182,7 @@ export type SealedLedgerEntry = LedgerEntry & {
179
182
  hash: string;
180
183
  } | null;
181
184
  };
182
- export declare const SCHEMA_VERSION = 131;
185
+ export declare const SCHEMA_VERSION = 133;
183
186
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
184
187
  export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
185
188
  /**
@@ -318,6 +321,10 @@ export declare const V129_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
318
321
  export declare const CONFINEMENT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_network (\n repo TEXT PRIMARY KEY,\n network_json TEXT NOT NULL CHECK (length(network_json) <= 16384),\n updated_at TEXT NOT NULL,\n updated_by TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS confinement_setting (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n unfenced_allowed INTEGER NOT NULL DEFAULT 0 CHECK (unfenced_allowed IN (0, 1)),\n updated_at TEXT,\n updated_by TEXT\n);\nINSERT OR IGNORE INTO confinement_setting (id, unfenced_allowed) VALUES (1, 0);\n";
319
322
  /** v131: where a project's builds may write (an approver's setting, agent-fence.ts WritesMode). */
320
323
  export declare const PROJECT_WRITES_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_writes (\n repo TEXT PRIMARY KEY,\n mode TEXT NOT NULL CHECK (mode IN ('workspace', 'anywhere')),\n updated_at TEXT NOT NULL,\n updated_by TEXT NOT NULL\n);\n";
324
+ /** v132: how far a project's checks reach beyond the check fence (check-access.ts CheckAccess). */
325
+ export declare const PROJECT_CHECK_ACCESS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_check_access (\n repo TEXT PRIMARY KEY,\n level TEXT NOT NULL CHECK (level IN ('sandboxed', 'sign-ins', 'unsandboxed')),\n updated_at TEXT NOT NULL,\n updated_by TEXT NOT NULL\n);\n";
326
+ /** v133: a project's sandbox settings (project-sandbox.ts): JSON lists, each sorted, distinct and valid. */
327
+ export declare const PROJECT_SANDBOX_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_sandbox (\n repo TEXT PRIMARY KEY,\n read_deny_json TEXT NOT NULL DEFAULT '[]',\n mach_lookup_json TEXT NOT NULL DEFAULT '[]',\n updated_at TEXT NOT NULL,\n updated_by TEXT NOT NULL\n);\n";
321
328
  /** v129: a finished run's How it ran, derived from its own records on first view (display only: never a verdict). Its
322
329
  * parser version says when to derive again; `steps` says how complete the step detail was, and why not. */
323
330
  export declare const RUN_EFFICIENCY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_efficiency (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n parser_version INTEGER NOT NULL,\n steps TEXT NOT NULL CHECK (steps IN ('complete','truncated','expired','absent')),\n steps_reason TEXT,\n payload_json TEXT NOT NULL CHECK (length(payload_json) <= 16384),\n derived_at TEXT NOT NULL\n);\n";
@@ -6135,6 +6142,9 @@ export declare class Store {
6135
6142
  recordRunFence(run: number, fence: {
6136
6143
  method: string;
6137
6144
  paths: number;
6145
+ policy?: 1 | 2;
6146
+ digest?: string;
6147
+ engine?: string;
6138
6148
  }, now: Date): void;
6139
6149
  /** What one attempt's network was and what its proxy saw (egress.ts), kept beside its fence. */
6140
6150
  recordRunNetwork(run: number, network: RunNetworkRecord, now: Date): void;
@@ -6142,6 +6152,9 @@ export declare class Store {
6142
6152
  runFence(run: number): {
6143
6153
  method: string;
6144
6154
  paths: number;
6155
+ policy?: 1 | 2;
6156
+ digest?: string;
6157
+ engine?: string;
6145
6158
  } | null;
6146
6159
  runTools(run: number): {
6147
6160
  tools: {
@@ -7174,6 +7187,50 @@ export declare class Store {
7174
7187
  * removed or expired — any of those means the composer's draft was accepted and must not be sent again). */
7175
7188
  leadRequestReceived(session: number, request: string): boolean;
7176
7189
  forgetQueuedLeadMessage(message: number): void;
7190
+ /** How far a project's checks reach beyond the check fence: its saved level, or sandboxed. */
7191
+ projectCheckAccess(repo: string): CheckAccess;
7192
+ /** Who last changed a project's check access, and when (null: never changed). */
7193
+ projectCheckAccessChange(repo: string): {
7194
+ by: string;
7195
+ at: string;
7196
+ } | null;
7197
+ /** The level a run's setup and checks ran at (its sealed approval's, or unsandboxed where no OS fence exists). Its
7198
+ * follow-up and batch checks never run wider than this. */
7199
+ stampRunCheckAccess(runId: number, level: CheckAccess, network?: NetworkProfile | null): void;
7200
+ /** The network a run's sign-ins checks ran under (null: none sealed; such a run's later checks run sandboxed). */
7201
+ runCheckNetwork(runId: number): NetworkProfile | null;
7202
+ /** The level a run's checks ran at: missing or unreadable reads as sandboxed. */
7203
+ runCheckAccess(runId: number): CheckAccess;
7204
+ /** A project's check access changed: saved, and in the ledger as before → after. `may` is what the caller proved:
7205
+ * "widen" only after an instance operator's password typed now; anything else may only narrow. The level is read again
7206
+ * and compared in the same transaction as the write, so a change made while the caller was proving itself can't turn
7207
+ * a narrowing into a widening. Refused: null, nothing written. */
7208
+ setProjectCheckAccess(repo: string, level: CheckAccess, by: string, now: Date, authority: {
7209
+ may: "narrow" | "widen";
7210
+ }): {
7211
+ before: CheckAccess;
7212
+ after: CheckAccess;
7213
+ } | null;
7214
+ /** The scout's browser ran outside Toolroll's fence under its launch contract (scout-browser.ts): which exception. */
7215
+ recordRunBrowser(run: number, browser: {
7216
+ containment: string;
7217
+ tool: string;
7218
+ }, now: Date): void;
7219
+ runBrowser(run: number): {
7220
+ containment: string;
7221
+ tool: string;
7222
+ } | null;
7223
+ /** A project's sandbox settings (project-sandbox.ts): its saved lists, or none. A damaged row keeps what it can read
7224
+ * of its read denies (never fewer) and grants nothing else. */
7225
+ projectSandbox(repo: string): ProjectSandbox;
7226
+ /** An approver changed a project's sandbox settings (their password was proved by the caller): saved whole, and in
7227
+ * the ledger as before → after. The account is proved again inside the write. Refused: null, nothing written. */
7228
+ setProjectSandbox(repo: string, wanted: ProjectSandbox, by: string, now: Date): {
7229
+ before: ProjectSandbox;
7230
+ after: ProjectSandbox;
7231
+ } | null;
7232
+ /** The fence policy a flow was made under (agent-fence.ts FencePolicy): v2 for one made from 0.9.70; v1 otherwise. */
7233
+ flowFencePolicy(flow: number): FencePolicy;
7177
7234
  }
7178
7235
  /** The receipt a bot's first pairing writes over pre-pairing history: an
7179
7236
  * explicit skip, with no delivered timestamp and no attempt, so suppressed
package/dist/store.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { WRITES_DEFAULT } from "./writes-mode.js";
2
- import { PROJECT_NETWORK_DEFAULT, projectNetworkFromValue, runNetworkFromValue } from "./egress.js";
2
+ import { checkAccessOf, widerThan } from "./check-access.js";
3
+ import { PROJECT_SANDBOX_DEFAULT, sandboxOf } from "./project-sandbox.js";
4
+ import { networkFromValue, PROJECT_NETWORK_DEFAULT, projectNetworkFromValue, runNetworkFromValue } from "./egress.js";
3
5
  import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
4
6
  import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
5
7
  import { reclaimDatabase } from "./database-reclaim.js";
@@ -718,7 +720,12 @@ const REGISTERED_AT_FILING = "again, as it was filed";
718
720
  // (confinement_setting, one row; off unless an instance operator turns it on).
719
721
  // v131 adds, and changes no saved row: where a project's builds may write (project_writes; agent-fence.ts, docs/plans/
720
722
  // agent-baseline-hardening.md arc 2) — only their workspace unless an approver chose anywhere.
721
- export const SCHEMA_VERSION = 131;
723
+ // v132 adds, and changes no saved row: how far a project's checks reach beyond the check fence (project_check_access;
724
+ // check-access.ts, docs/plans/check-access.md) — sandboxed unless an instance operator widened it.
725
+ // v133 adds, and changes no saved row: a project's sandbox settings (project_sandbox; project-sandbox.ts,
726
+ // docs/plans/sandbox-parity.md C9) — what its builds may not read, the macOS services and protected files they may — and
727
+ // the fence policy a flow was made under (flow.fence_policy; saved flows read NULL: v1, the home configuration denied).
728
+ export const SCHEMA_VERSION = 133;
722
729
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
723
730
  export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
724
731
  /** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
@@ -851,7 +858,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
851
858
  * unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
852
859
  * (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
853
860
  */
854
- export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131]);
861
+ export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133]);
855
862
  /** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
856
863
  export const V119_ADDED_COLUMNS = Object.freeze({
857
864
  task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
@@ -988,6 +995,25 @@ CREATE TABLE IF NOT EXISTS project_writes (
988
995
  updated_by TEXT NOT NULL
989
996
  );
990
997
  `;
998
+ /** v132: how far a project's checks reach beyond the check fence (check-access.ts CheckAccess). */
999
+ export const PROJECT_CHECK_ACCESS_SCHEMA = `
1000
+ CREATE TABLE IF NOT EXISTS project_check_access (
1001
+ repo TEXT PRIMARY KEY,
1002
+ level TEXT NOT NULL CHECK (level IN ('sandboxed', 'sign-ins', 'unsandboxed')),
1003
+ updated_at TEXT NOT NULL,
1004
+ updated_by TEXT NOT NULL
1005
+ );
1006
+ `;
1007
+ /** v133: a project's sandbox settings (project-sandbox.ts): JSON lists, each sorted, distinct and valid. */
1008
+ export const PROJECT_SANDBOX_SCHEMA = `
1009
+ CREATE TABLE IF NOT EXISTS project_sandbox (
1010
+ repo TEXT PRIMARY KEY,
1011
+ read_deny_json TEXT NOT NULL DEFAULT '[]',
1012
+ mach_lookup_json TEXT NOT NULL DEFAULT '[]',
1013
+ updated_at TEXT NOT NULL,
1014
+ updated_by TEXT NOT NULL
1015
+ );
1016
+ `;
991
1017
  /** v129: a finished run's How it ran, derived from its own records on first view (display only: never a verdict). Its
992
1018
  * parser version says when to derive again; `steps` says how complete the step detail was, and why not. */
993
1019
  export const RUN_EFFICIENCY_SCHEMA = `
@@ -3923,6 +3949,15 @@ function initializeStore(db, file) {
3923
3949
  db.exec(CONFINEMENT_SCHEMA);
3924
3950
  // v131: where a project's builds may write (a new table only).
3925
3951
  db.exec(PROJECT_WRITES_SCHEMA);
3952
+ // v132: how far a project's checks reach beyond the check fence (a new table), and the level each build's checks ran
3953
+ // at (run.sandbox_access; saved rows read NULL: sandboxed).
3954
+ db.exec(PROJECT_CHECK_ACCESS_SCHEMA);
3955
+ addColumn(db, "run", "sandbox_access", "TEXT");
3956
+ addColumn(db, "run", "sandbox_network", "TEXT");
3957
+ // v133: a project's sandbox settings (a new table), and the fence policy each flow was made under (saved flows: NULL,
3958
+ // read as v1).
3959
+ db.exec(PROJECT_SANDBOX_SCHEMA);
3960
+ addColumn(db, "flow", "fence_policy", "INTEGER");
3926
3961
  // v123: a flow that runs on its own (flow-unattended.ts): its grants and daily runs (new tables), and which authority
3927
3962
  // armed a plan's unchanged-plan approval (plan_authorization.basis, V123_ADDED_COLUMNS: saved rows read the mode).
3928
3963
  db.exec(FLOW_UNATTENDED_SCHEMA);
@@ -6729,6 +6764,23 @@ export class Store {
6729
6764
  .get(taskId);
6730
6765
  if (writtenByLead !== undefined)
6731
6766
  return false;
6767
+ // CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
6768
+ // all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
6769
+ const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
6770
+ let sealedAccess;
6771
+ // SANDBOX WIDENINGS (v133, project-sandbox.ts): extra macOS services seal, likewise,
6772
+ // only on a person's own password yes to exactly them.
6773
+ let sandboxWidened = false;
6774
+ try {
6775
+ const stored = typeof profileText === "string" ? JSON.parse(profileText) : null;
6776
+ sealedAccess = stored?.profile?.["checkAccess"];
6777
+ sandboxWidened = stored?.profile?.["machLookup"] !== undefined;
6778
+ }
6779
+ catch {
6780
+ sealedAccess = "unreadable";
6781
+ }
6782
+ if (sealedAccess !== undefined || sandboxWidened)
6783
+ return false;
6732
6784
  }
6733
6785
  // SEPARATION OF DUTIES (v102), enforced in the primitive so no road
6734
6786
  // around it exists: a project's rules can refuse the requester, and
@@ -15223,7 +15275,8 @@ export class Store {
15223
15275
  // ---- flows (v81) -------------------------------------------------------------
15224
15276
  createFlow(flow, now) {
15225
15277
  const stamp = now.toISOString();
15226
- return Number(this.db.prepare("INSERT INTO flow (repo, name, definition_json, revision, state, created_by, created_at, updated_by, updated_at) VALUES (?, ?, ?, 1, 'active', ?, ?, ?, ?)")
15278
+ // A flow made from 0.9.70 on runs under fence policy v2 (agent-fence.ts FencePolicy); one made before stays v1.
15279
+ return Number(this.db.prepare("INSERT INTO flow (repo, name, definition_json, revision, state, created_by, created_at, updated_by, updated_at, fence_policy) VALUES (?, ?, ?, 1, 'active', ?, ?, ?, ?, 2)")
15227
15280
  .run(flow.repo, flow.name, flowDefinitionForStore(flow.definitionJson), flow.by, stamp, flow.by, stamp).lastInsertRowid);
15228
15281
  }
15229
15282
  /** Hand a flow to another person: its "the owner decides" zones ask them from now on. */
@@ -18987,6 +19040,115 @@ export class Store {
18987
19040
  this.db.prepare("UPDATE lead_queue SET context_json = NULL WHERE message = ?").run(message);
18988
19041
  this.db.prepare("DELETE FROM memory_search WHERE kind = 'conversation' AND ref = ?").run(`message:${message}`);
18989
19042
  }
19043
+ // ---- check access (v132, check-access.ts). Kept last in the class: the freshness tripwire pins method positions.
19044
+ /** How far a project's checks reach beyond the check fence: its saved level, or sandboxed. */
19045
+ projectCheckAccess(repo) {
19046
+ // A damaged row reads as the narrowest level, never a wider one.
19047
+ return checkAccessOf(this.db.prepare("SELECT level FROM project_check_access WHERE repo = ?").get(repo)?.["level"]);
19048
+ }
19049
+ /** Who last changed a project's check access, and when (null: never changed). */
19050
+ projectCheckAccessChange(repo) {
19051
+ const row = this.db.prepare("SELECT updated_by, updated_at FROM project_check_access WHERE repo = ?").get(repo);
19052
+ return row === undefined ? null : { by: String(row["updated_by"]), at: String(row["updated_at"]) };
19053
+ }
19054
+ /** The level a run's setup and checks ran at (its sealed approval's, or unsandboxed where no OS fence exists). Its
19055
+ * follow-up and batch checks never run wider than this. */
19056
+ stampRunCheckAccess(runId, level, network = null) {
19057
+ this.db.prepare("UPDATE run SET sandbox_access = ?, sandbox_network = ? WHERE id = ?").run(level, network === null ? null : JSON.stringify(network), runId);
19058
+ }
19059
+ /** The network a run's sign-ins checks ran under (null: none sealed; such a run's later checks run sandboxed). */
19060
+ runCheckNetwork(runId) {
19061
+ const text = this.db.prepare("SELECT sandbox_network FROM run WHERE id = ?").get(runId)?.["sandbox_network"];
19062
+ if (typeof text !== "string")
19063
+ return null;
19064
+ try {
19065
+ return networkFromValue(JSON.parse(text));
19066
+ }
19067
+ catch {
19068
+ return null;
19069
+ }
19070
+ }
19071
+ /** The level a run's checks ran at: missing or unreadable reads as sandboxed. */
19072
+ runCheckAccess(runId) {
19073
+ return checkAccessOf(this.db.prepare("SELECT sandbox_access FROM run WHERE id = ?").get(runId)?.["sandbox_access"]);
19074
+ }
19075
+ /** A project's check access changed: saved, and in the ledger as before → after. `may` is what the caller proved:
19076
+ * "widen" only after an instance operator's password typed now; anything else may only narrow. The level is read again
19077
+ * and compared in the same transaction as the write, so a change made while the caller was proving itself can't turn
19078
+ * a narrowing into a widening. Refused: null, nothing written. */
19079
+ setProjectCheckAccess(repo, level, by, now, authority) {
19080
+ return this.transact(() => {
19081
+ const before = this.projectCheckAccess(repo);
19082
+ // Who is acting is proved again here too: an account revoked, demoted or moved off the project while its password
19083
+ // was being checked changes nothing.
19084
+ const account = this.accountOf(by);
19085
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !this.accountCanAccess(by, repo))
19086
+ return null;
19087
+ if (widerThan(level, before) && (authority.may !== "widen" || !this.isInstanceOperator(by)))
19088
+ return null;
19089
+ this.db.prepare(`INSERT INTO project_check_access (repo, level, updated_at, updated_by) VALUES (?, ?, ?, ?)
19090
+ ON CONFLICT (repo) DO UPDATE SET level = excluded.level, updated_at = excluded.updated_at, updated_by = excluded.updated_by`).run(repo, level, now.toISOString(), by);
19091
+ const after = this.projectCheckAccess(repo);
19092
+ this.recordPolicy(by, repo, "check access", before, after, now);
19093
+ return { before, after };
19094
+ });
19095
+ }
19096
+ // ---- sandbox parity (docs/plans/sandbox-parity.md). Kept last in the class: the freshness tripwire pins positions.
19097
+ /** The scout's browser ran outside Toolroll's fence under its launch contract (scout-browser.ts): which exception. */
19098
+ recordRunBrowser(run, browser, now) {
19099
+ const json = JSON.stringify(browser);
19100
+ this.db.prepare(`INSERT INTO run_tool (run, tools_json, created_at) VALUES (?, json_object('browser', json(?)), ?)
19101
+ ON CONFLICT (run) DO UPDATE SET tools_json = json_set(tools_json, '$.browser', json(?))`).run(run, json, now.toISOString(), json);
19102
+ }
19103
+ runBrowser(run) {
19104
+ const row = this.db.prepare("SELECT json_extract(tools_json, '$.browser') AS browser FROM run_tool WHERE run = ?").get(run);
19105
+ if (row === undefined || row["browser"] === null)
19106
+ return null;
19107
+ try {
19108
+ const parsed = JSON.parse(String(row["browser"]));
19109
+ return typeof parsed.containment === "string" && typeof parsed.tool === "string" ? { containment: parsed.containment, tool: parsed.tool } : null;
19110
+ }
19111
+ catch {
19112
+ return null;
19113
+ }
19114
+ }
19115
+ /** A project's sandbox settings (project-sandbox.ts): its saved lists, or none. A damaged row keeps what it can read
19116
+ * of its read denies (never fewer) and grants nothing else. */
19117
+ projectSandbox(repo) {
19118
+ const row = this.db.prepare("SELECT read_deny_json, mach_lookup_json FROM project_sandbox WHERE repo = ?").get(repo);
19119
+ if (row === undefined)
19120
+ return { ...PROJECT_SANDBOX_DEFAULT };
19121
+ const read = (text) => { try {
19122
+ return typeof text === "string" ? JSON.parse(text) : null;
19123
+ }
19124
+ catch {
19125
+ return null;
19126
+ } };
19127
+ return sandboxOf({ readDeny: read(row["read_deny_json"]), machLookup: read(row["mach_lookup_json"]) });
19128
+ }
19129
+ /** An approver changed a project's sandbox settings (their password was proved by the caller): saved whole, and in
19130
+ * the ledger as before → after. The account is proved again inside the write. Refused: null, nothing written. */
19131
+ setProjectSandbox(repo, wanted, by, now) {
19132
+ return this.transact(() => {
19133
+ const account = this.accountOf(by);
19134
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !this.accountCanAccess(by, repo))
19135
+ return null;
19136
+ const before = this.projectSandbox(repo);
19137
+ const clean = sandboxOf(wanted);
19138
+ this.db.prepare(`INSERT INTO project_sandbox (repo, read_deny_json, mach_lookup_json, updated_at, updated_by) VALUES (?, ?, ?, ?, ?)
19139
+ ON CONFLICT (repo) DO UPDATE SET read_deny_json = excluded.read_deny_json, mach_lookup_json = excluded.mach_lookup_json,
19140
+ updated_at = excluded.updated_at, updated_by = excluded.updated_by`)
19141
+ .run(repo, JSON.stringify(clean.readDeny), JSON.stringify(clean.machLookup), now.toISOString(), by);
19142
+ const after = this.projectSandbox(repo);
19143
+ const words = (one) => `read-deny ${one.readDeny.length === 0 ? "none" : one.readDeny.join(",")}; mach ${one.machLookup.length === 0 ? "none" : one.machLookup.join(",")}`;
19144
+ this.recordPolicy(by, repo, "sandbox", words(before), words(after), now);
19145
+ return { before, after };
19146
+ });
19147
+ }
19148
+ /** The fence policy a flow was made under (agent-fence.ts FencePolicy): v2 for one made from 0.9.70; v1 otherwise. */
19149
+ flowFencePolicy(flow) {
19150
+ return this.db.prepare("SELECT fence_policy FROM flow WHERE id = ?").get(flow)?.["fence_policy"] === 2 ? 2 : 1;
19151
+ }
18990
19152
  }
18991
19153
  /** A stored count or id that must be a whole, non-negative number — a
18992
19154
  * NULL, an empty string, a fraction, or text reads as null (v48 authority repair). */
@@ -13,6 +13,7 @@ export type ToolIo = {
13
13
  callTool?: ToolCaller;
14
14
  listTools?: ToolLister;
15
15
  toolHome?: string | undefined;
16
+ fenceAvailable?: () => boolean;
16
17
  };
17
18
  /** Actions whose names say they only read start as "do it"; everything else asks first until its manager says otherwise. */
18
19
  export declare const READ_VERB: RegExp;
@@ -19,7 +19,7 @@
19
19
  import { redactSecretLines, scanForSecrets } from "./evidence.js";
20
20
  import { scrubSecrets } from "./flow-secrets.js";
21
21
  import { toolRefusal } from "./policy.js";
22
- import { callProjectTool, listProjectToolActions, projectToolsOf, readToolSecrets } from "./project-tools.js";
22
+ import { callProjectTool, listProjectToolActions, projectToolsOf, readToolSecrets, toolServerFor } from "./project-tools.js";
23
23
  import { ALL_CREDENTIAL_ENV } from "./provider.js";
24
24
  /** Actions whose names say they only read start as "do it"; everything else asks first until its manager says otherwise. */
25
25
  export const READ_VERB = /^(get|list|search|find|read|fetch|lookup|look_up|query|show|describe|count|check|view|retrieve)([_.-]|$)/i;
@@ -113,13 +113,15 @@ export async function refreshGrants(store, mate, now, io) {
113
113
  const tool = tools.find(one => one.name === grant.tool);
114
114
  if (tool === undefined)
115
115
  continue;
116
- const listed = await listed_(tool, mate.repo, io);
116
+ const listed = await listed_(store, tool, mate.repo, io);
117
117
  if (!listed.ok)
118
118
  continue;
119
119
  store.saveSubagentGrant({ subagent: mate.id, tool: grant.tool, ...withActions(grant.rules, listed.actions), listedAt: now.toISOString() }, grant.updatedBy, now);
120
120
  }
121
121
  }
122
- const listed_ = (tool, repo, io) => (io.listTools ?? ((spec, values) => listProjectToolActions(spec, values, { timeoutMs: 60_000, omitEnv: ALL_CREDENTIAL_ENV })))(tool.spec, readToolSecrets(repo, tool.name, io.toolHome));
122
+ const listed_ = (store, tool, repo, io) => (io.listTools ?? ((spec, values) => listProjectToolActions(spec, values, { timeoutMs: 60_000, omitEnv: ALL_CREDENTIAL_ENV, server: serverOf(store, repo, io) })))(tool.spec, readToolSecrets(repo, tool.name, io.toolHome));
123
+ /** Where a subagent's tool server starts: in the server fence (project-tools.ts toolServerFor), never bare. */
124
+ const serverOf = (store, repo, io) => toolServerFor(store, repo, { ...(io.toolHome === undefined ? {} : { home: io.toolHome }), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }) });
123
125
  /** Names only, from the tool's last test: what a grant starts with when it can't be listed right now. */
124
126
  const namesOnly = (tool) => (tool.lastTest?.tools ?? []).map(name => ({ name, about: "", input: null, readOnly: false }));
125
127
  /** Let a subagent use one of the project's tools. Its actions start at their defaults: reading is free, everything else asks first. */
@@ -129,7 +131,7 @@ export async function grantTool(store, mate, tool, by, now, io = {}) {
129
131
  return { ok: false, said: `This project has no tool called ${tool}. Add it on the Tools page first.` };
130
132
  if (store.subagentGrant(mate.id, tool) !== null)
131
133
  return { ok: true, said: `It can already use ${tool}.` };
132
- const listed = await listed_(found, mate.repo, io);
134
+ const listed = await listed_(store, found, mate.repo, io);
133
135
  return grantListed(store, mate, found, listed.ok ? listed.actions : null, by, now);
134
136
  }
135
137
  /** A grant from what the tool offers (or, not reachable now, the names its last test found). */
@@ -219,7 +221,7 @@ export async function makeCall(store, call, repo, io, now, options = {}) {
219
221
  const values = readToolSecrets(repo, tool.name, io.toolHome);
220
222
  let answer;
221
223
  try {
222
- answer = await (io.callTool ?? ((spec, secrets, name, given) => callProjectTool(spec, secrets, name, given, { timeoutMs: 120_000, omitEnv: ALL_CREDENTIAL_ENV })))(tool.spec, values, call.action, call.input);
224
+ answer = await (io.callTool ?? ((spec, secrets, name, given) => callProjectTool(spec, secrets, name, given, { timeoutMs: 120_000, omitEnv: ALL_CREDENTIAL_ENV, server: serverOf(store, repo, io) })))(tool.spec, values, call.action, call.input);
223
225
  }
224
226
  catch (error) {
225
227
  answer = { ok: false, problem: error instanceof Error ? error.message : "It couldn't be reached." };
package/dist/surface.js CHANGED
@@ -353,6 +353,7 @@ const GUIDE_ROWS = [
353
353
  operator("runner bind", "replace which repositories a worker may build in"),
354
354
  operator("runner capacity", "set how many tasks a worker runs at once; running work carries on, the new number applies at the next claim"),
355
355
  operator("project checks", "show or set a project's batch checks (--batch on|off, off by default): results that finish within 10 minutes share one full check on a temporary batch commit; nothing is merged into a real branch"),
356
+ operator("project check-access", "show or set how far a project's checks reach: sandboxed (the default), with model and GitHub sign-ins, or not sandboxed; an instance operator widens it with a password typed now"),
356
357
  operator("project concurrency", "set how many of a project's tasks build at once (default 2, never past the worker's capacity); running work carries on"),
357
358
  operator("coordinator mint", "mint the MCP filing credential — repo-bound, rate-limited, token shown once"),
358
359
  operator("coordinator revoke", "revoke an MCP filing credential"),
@@ -450,7 +451,7 @@ const REMOTE_STEP_UP = [
450
451
  // A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
451
452
  "tokens create", "tokens list", "tokens revoke", "tokens rotate",
452
453
  "mode show", "mode set", "mode revoke", "chat-approval on", "review on", "review off", "config set", "config clear",
453
- "verify set", "verify clear", "verify level", "project checks", "project concurrency", "intake grant", "intake clear", "webhook primary",
454
+ "verify set", "verify clear", "verify level", "project checks", "project check-access", "project concurrency", "intake grant", "intake clear", "webhook primary",
454
455
  ];
455
456
  const REMOTE_YES = [
456
457
  "status", "integrations", "ready", "gaps", "grants", "sync", "audit",
@@ -19,9 +19,10 @@ export type BrokerOptions = {
19
19
  /** Why a local server may not start unwrapped (no fence for it here), read when it would; null when it may. */
20
20
  unwrapped?: () => string | null;
21
21
  /** How a local server is started inside its run's fence, read when it starts (the fence is fixed after the tools). */
22
- wrap?: () => ((file: string, args: readonly string[]) => {
22
+ wrap?: () => ((file: string, args: readonly string[], cwd?: string) => {
23
23
  file: string;
24
24
  args: string[];
25
+ env?: Record<string, string>;
25
26
  }) | null;
26
27
  };
27
28
  export type ToolBroker = {