toolroll 0.9.71 → 0.9.73

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 (131) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/agent-fence.d.ts +10 -2
  3. package/dist/agent-fence.js +47 -9
  4. package/dist/approval-authority.d.ts +20 -0
  5. package/dist/approval-authority.js +67 -0
  6. package/dist/assignment.js +26 -4
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +28 -28
  9. package/dist/builder.js +48 -6
  10. package/dist/chat-actions.js +13 -8
  11. package/dist/chat-decide.d.ts +47 -1
  12. package/dist/chat-decide.js +186 -5
  13. package/dist/check-access.js +2 -2
  14. package/dist/check-report.d.ts +109 -0
  15. package/dist/check-report.js +205 -0
  16. package/dist/claim.js +1 -1
  17. package/dist/coding-provider.js +4 -2
  18. package/dist/commit-extract.d.ts +111 -0
  19. package/dist/commit-extract.js +514 -0
  20. package/dist/contracts/console-api.d.ts +4 -4
  21. package/dist/contracts/console-api.js +5 -4
  22. package/dist/contracts/flow.d.ts +16 -0
  23. package/dist/contracts/flow.js +3 -0
  24. package/dist/contracts/release-check.d.ts +335 -0
  25. package/dist/contracts/release-check.js +159 -0
  26. package/dist/contracts/release-terms.d.ts +66 -0
  27. package/dist/contracts/release-terms.js +74 -0
  28. package/dist/contracts/verification-receipt.d.ts +42 -8
  29. package/dist/contracts/verification-receipt.js +24 -7
  30. package/dist/contracts/verification-view.d.ts +1 -0
  31. package/dist/deploy-gate.d.ts +17 -0
  32. package/dist/deploy-gate.js +68 -0
  33. package/dist/dispatch.js +3 -10
  34. package/dist/dispose.js +4 -1
  35. package/dist/evidence-pack.d.ts +1 -1
  36. package/dist/evidence-pack.js +20 -2
  37. package/dist/evidence.d.ts +1 -0
  38. package/dist/evidence.js +2 -1
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.js +7 -3
  41. package/dist/flow-engine.js +5 -1
  42. package/dist/flow-gallery-ui.js +4 -2
  43. package/dist/flow-gallery.d.ts +3 -0
  44. package/dist/flow-gallery.js +6 -2
  45. package/dist/flow-pull-request.d.ts +5 -2
  46. package/dist/flow-pull-request.js +14 -5
  47. package/dist/flow-schedule.js +25 -6
  48. package/dist/flow-triggers.d.ts +8 -0
  49. package/dist/flow-triggers.js +15 -0
  50. package/dist/flows-cli.js +13 -3
  51. package/dist/flows-ui.js +7 -1
  52. package/dist/guides.js +10 -1
  53. package/dist/invoke.d.ts +17 -6
  54. package/dist/invoke.js +48 -18
  55. package/dist/kits-ui.js +4 -2
  56. package/dist/kits.d.ts +6 -4
  57. package/dist/kits.js +9 -11
  58. package/dist/lead-status.js +5 -2
  59. package/dist/live-terminal.d.ts +10 -6
  60. package/dist/live-terminal.js +55 -14
  61. package/dist/mcp-connect.d.ts +10 -0
  62. package/dist/mcp-connect.js +2 -0
  63. package/dist/nightly-check.d.ts +104 -0
  64. package/dist/nightly-check.js +362 -0
  65. package/dist/operate.js +39 -2
  66. package/dist/person-proof.d.ts +2 -0
  67. package/dist/person-proof.js +14 -0
  68. package/dist/plan-auto.js +4 -4
  69. package/dist/planner-source.d.ts +1 -1
  70. package/dist/project-delete-ui.js +1 -1
  71. package/dist/project-tools.js +3 -1
  72. package/dist/proof.js +2 -0
  73. package/dist/protected-storage.d.ts +67 -0
  74. package/dist/protected-storage.js +398 -0
  75. package/dist/publish.js +156 -144
  76. package/dist/pull-request-flow.d.ts +2 -0
  77. package/dist/pull-request-flow.js +24 -2
  78. package/dist/release-adapters.d.ts +16 -1
  79. package/dist/release-adapters.js +292 -29
  80. package/dist/release-authority.d.ts +118 -0
  81. package/dist/release-authority.js +135 -0
  82. package/dist/release-card.d.ts +76 -0
  83. package/dist/release-card.js +252 -0
  84. package/dist/release-carry.d.ts +236 -0
  85. package/dist/release-carry.js +664 -0
  86. package/dist/release-cli.d.ts +3 -0
  87. package/dist/release-cli.js +158 -6
  88. package/dist/release-coverage.js +3 -0
  89. package/dist/release-git.d.ts +43 -0
  90. package/dist/release-git.js +106 -0
  91. package/dist/release-recheck.d.ts +146 -0
  92. package/dist/release-recheck.js +657 -0
  93. package/dist/release-view.d.ts +34 -0
  94. package/dist/release-view.js +27 -0
  95. package/dist/release.d.ts +139 -3
  96. package/dist/release.js +641 -250
  97. package/dist/remote-main.d.ts +2 -0
  98. package/dist/remote-main.js +1 -1
  99. package/dist/result-completion.d.ts +6 -0
  100. package/dist/result-completion.js +15 -1
  101. package/dist/result-follow-ups.js +29 -1
  102. package/dist/sandbox-runtime.d.ts +16 -5
  103. package/dist/sandbox-runtime.js +19 -6
  104. package/dist/scope.d.ts +122 -5
  105. package/dist/scope.js +236 -7
  106. package/dist/server/flows.js +40 -10
  107. package/dist/server/render-settings.js +1 -1
  108. package/dist/server/render-tasks.d.ts +8 -0
  109. package/dist/server/render-tasks.js +50 -9
  110. package/dist/server/route-table.js +1 -0
  111. package/dist/server/settings.js +26 -5
  112. package/dist/server/task-views.js +4 -0
  113. package/dist/server/tasks.js +12 -0
  114. package/dist/setup-guide.d.ts +2 -2
  115. package/dist/store.d.ts +230 -5
  116. package/dist/store.js +733 -37
  117. package/dist/subagent-memory.d.ts +1 -0
  118. package/dist/subagent-memory.js +1 -1
  119. package/dist/subagent-tools.d.ts +22 -6
  120. package/dist/subagent-tools.js +44 -8
  121. package/dist/subagent-work.d.ts +2 -0
  122. package/dist/subagent-work.js +5 -2
  123. package/dist/subagents-ui.js +6 -4
  124. package/dist/task-outcome-cli.js +7 -3
  125. package/dist/telegram.d.ts +6 -1
  126. package/dist/telegram.js +58 -6
  127. package/dist/toolroll-update.js +2 -2
  128. package/dist/verification-evidence.d.ts +25 -1
  129. package/dist/verification-evidence.js +57 -2
  130. package/dist/work-index.js +3 -6
  131. package/package.json +1 -1
package/dist/operate.js CHANGED
@@ -56,6 +56,8 @@ import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
56
56
  import { sendSubagentSummaries } from "./subagent-admin.js";
57
57
  import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
58
58
  import { refreshConnections } from "./mcp-connect.js";
59
+ import { releaseTermsFor } from "./release-carry.js";
60
+ import { refreshTrustedMains, settleReleaseChecks } from "./nightly-check.js";
59
61
  import { SECRETS_FIRST, startToolSecrets, toolSecretStorage } from "./project-tools.js";
60
62
  import { followDiscord } from "./discord.js";
61
63
  import { followTeams } from "./teams.js";
@@ -192,7 +194,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
192
194
  import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
193
195
  import { serveMcp } from "./mcp.js";
194
196
  import { createInterface } from "node:readline";
195
- import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin } from "./scope.js";
197
+ import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin, personApprovalOf } from "./scope.js";
196
198
  import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
197
199
  import { WorktreePool } from "./worktree.js";
198
200
  import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
@@ -694,8 +696,12 @@ export const OPERATE_VALUE_FLAGS = new Set([
694
696
  "request",
695
697
  // project sandbox (sandbox parity C9): read denies and macOS services.
696
698
  "read-deny", "mach",
699
+ // task scope --release <version>:<pr>:<branch>: a release scope's terms (release-once.md).
700
+ "release",
697
701
  ]);
698
702
  export const OPERATE_BOOLEAN_FLAGS = new Set([
703
+ // task scope --release ... --no-carry: every later commit asks again.
704
+ "no-carry",
699
705
  "json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
700
706
  "clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
701
707
  "no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
@@ -6225,6 +6231,11 @@ async function runWatchLoop(args) {
6225
6231
  const ran = await runWaitingChecks(store, context.evidenceRoot, { now: context.clock, shouldStop: stopping });
6226
6232
  if (ran > 0)
6227
6233
  progress(`watch: ran ${ran} follow-up check(s)`);
6234
+ // The nightly check of main and the release checks that clear its failures (release-once.md, part 3).
6235
+ await refreshTrustedMains(store, context.evidenceRoot, context.clock(), context.gitRunner);
6236
+ const settledChecks = settleReleaseChecks(store, context.evidenceRoot, context.clock());
6237
+ if (settledChecks > 0)
6238
+ progress(`watch: settled ${settledChecks} release check(s)`);
6228
6239
  // A finished check may send a build's card down its failure path: now, not on the next beat.
6229
6240
  if (ran + batched > 0)
6230
6241
  moveCards(store, repo, context.clock(), context.evidenceRoot);
@@ -9031,6 +9042,8 @@ function showTask(positional, context) {
9031
9042
  claim: currentClaim(store, ref.id, now),
9032
9043
  scope,
9033
9044
  approval: approvalOf(scope),
9045
+ // Whether a person gave that approval, with their proof (approval policy): what the release gate reads.
9046
+ personApproval: personApprovalOf(store, id),
9034
9047
  route: taskRouteView(routed, readiness),
9035
9048
  // Fresh base: the commit this task's work starts from, and how far current main has moved past it.
9036
9049
  base,
@@ -9719,6 +9732,25 @@ async function scopeTask(positional, flags, context) {
9719
9732
  const candidateGiven = text(flags, "candidate");
9720
9733
  if (candidateGiven !== undefined && !isCommitSha(candidateGiven))
9721
9734
  return fail(write, json, "task scope", "usage", "--candidate is the full 40-character commit hash the machine will check out", EXIT.usage);
9735
+ // A release scope's terms (release-once.md), filled in from the plane's own records: `--release
9736
+ // <version>:<pull request>:<branch>` with the candidate. Terms that can't be made (the checks run outside the
9737
+ // sandbox, say) file the scope without them: every later commit then asks again, as before.
9738
+ const releaseGiven = text(flags, "release");
9739
+ let releaseTerms;
9740
+ let releaseNote = null;
9741
+ if (releaseGiven !== undefined) {
9742
+ const spec = /^(\d+\.\d+\.\d+):([1-9][0-9]*):([A-Za-z0-9._/-]+)$/.exec(releaseGiven);
9743
+ if (spec === null || candidateGiven === undefined)
9744
+ return fail(write, json, "task scope", "usage", "--release takes <version>:<pull request>:<branch>, with --candidate <commit>", EXIT.usage);
9745
+ const repo = store.lookupRef(id)?.repo ?? null;
9746
+ if (repo === null)
9747
+ return fail(write, json, "task scope", "usage", "a release scope's task has to be placed in its project first", EXIT.usage);
9748
+ const made = releaseTermsFor(store, { repo, version: spec[1], pr: Number(spec[2]), branch: spec[3], candidate: candidateGiven, now, noCarry: flags.get("no-carry") === true });
9749
+ if (made.ok)
9750
+ releaseTerms = made.terms;
9751
+ else
9752
+ releaseNote = `Later commits on this release will each ask for approval: ${made.why}.`;
9753
+ }
9722
9754
  if (acceptanceParse.criteria.length === 0)
9723
9755
  return fail(write, json, "task scope", "acceptance-required", "--acceptance named no valid criteria", EXIT.usage);
9724
9756
  const touches = (text(flags, "touches") ?? "").split(",").map(one => one.trim()).filter(Boolean);
@@ -9785,6 +9817,7 @@ async function scopeTask(positional, flags, context) {
9785
9817
  : null;
9786
9818
  const proposed = propose(store, {
9787
9819
  ...(candidateGiven === undefined ? {} : { candidate: candidateGiven }),
9820
+ ...(releaseTerms === undefined ? {} : { releaseTerms }),
9788
9821
  taskId: id,
9789
9822
  goal,
9790
9823
  outOfScope: text(flags, "not") ?? null,
@@ -9836,7 +9869,8 @@ async function scopeTask(positional, flags, context) {
9836
9869
  }
9837
9870
  // A changed scope on a planning task plans again (v119): say so once, instead of leaving the old plan implied.
9838
9871
  const replanned = (store.lookupRef(id)?.planningGeneration ?? 0) > generationBefore;
9839
- return succeed(write, json, "task scope", { scope, ...(replanned ? { planningRestarted: true } : {}) }, () => [
9872
+ return succeed(write, json, "task scope", { scope, ...(replanned ? { planningRestarted: true } : {}), ...(releaseNote === null ? {} : { releaseNote }) }, () => [
9873
+ ...(releaseNote === null ? [] : [releaseNote]),
9840
9874
  replanned
9841
9875
  ? `Scope written for ${id}. The scope changed, so planning starts again; nothing builds until somebody approves the new plan.`
9842
9876
  : `Scope written for ${id}. Nothing will build it until somebody approves it.`,
@@ -10059,6 +10093,9 @@ function describeApproveFailure(reason, id) {
10059
10093
  return `you filed ${id}, and this project needs someone else to approve it`;
10060
10094
  if (reason === "person-required")
10061
10095
  return `${id} is protected work: a person has to approve it, not an operating mode, a schedule or a subagent`;
10096
+ // Approval policy, phase 1: the lead's token (or a remote API token) proves a credential, not a person.
10097
+ if (reason === "machine-proof")
10098
+ return "only a person can approve this — approve it on the task page or from your paired chat";
10062
10099
  if (reason === "second-approver")
10063
10100
  return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
10064
10101
  if (reason === "no-approvers") {
@@ -0,0 +1,2 @@
1
+ /** Whether a recorded proof is a person's. A runtime string: a missing, corrupt or future value is never a person. */
2
+ export declare function personProof(proof: string | null | undefined): boolean;
@@ -0,0 +1,14 @@
1
+ /** Every credential method, classified: a person proved now (or by their own remembered sign-in), or a machine
2
+ * credential standing in for them. A new method fails to compile here until someone decides which it is. */
3
+ const METHOD_IS_PERSON = {
4
+ "password-now": true, "sso-now": true, "fresh-password": true, "fresh-sso": true, "session": true, "cli-login": true,
5
+ "remote-token": false, "lead-token": false,
6
+ };
7
+ /** Proofs that aren't credential methods: the paired-chat tap, and the phone plan's passkey ceremony. */
8
+ const OTHER_PERSON_PROOFS = new Set(["chat", "passkey"]);
9
+ /** Whether a recorded proof is a person's. A runtime string: a missing, corrupt or future value is never a person. */
10
+ export function personProof(proof) {
11
+ if (typeof proof !== "string")
12
+ return false;
13
+ return Object.prototype.hasOwnProperty.call(METHOD_IS_PERSON, proof) ? METHOD_IS_PERSON[proof] : OTHER_PERSON_PROOFS.has(proof);
14
+ }
package/dist/plan-auto.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { releaseUnattendedRun, reserveUnattendedRun, unattendedGrantStands } from "./flow-unattended.js";
2
2
  import { modeTermsFromJson } from "./modes.js";
3
3
  import { plannerContractOf, plannerSourceDigest } from "./planner-source.js";
4
- import { modeFilingCoverage, propose } from "./scope.js";
4
+ import { modeFilingCoverage, propose, quarantinedAuthor } from "./scope.js";
5
5
  export const PLAN_AUTO_SCHEMA = `CREATE TABLE IF NOT EXISTS plan_authorization (
6
6
  task_id TEXT PRIMARY KEY REFERENCES task(id),
7
7
  source_digest TEXT NOT NULL,
@@ -21,7 +21,7 @@ export function applyModeToNewFiling(store, taskId, actor, now) {
21
21
  const ref = store.lookupRef(taskId);
22
22
  const scope = store.getScope(taskId);
23
23
  const coverage = modeFilingCoverage(store, ref?.repo ?? null, actor, now);
24
- if (ref === null || scope === null || scope.proposedVia != null || coverage === null)
24
+ if (ref === null || scope === null || quarantinedAuthor(scope.proposedVia) || coverage === null)
25
25
  return;
26
26
  const filed = propose(store, { taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance,
27
27
  budgetMicrousd: scope.budgetMicrousd ?? coverage.defaultBudgetMicrousd,
@@ -48,7 +48,7 @@ export function authorizePlanUnderMode(store, taskId, actor, now) {
48
48
  if (!source.ok || source.contract.scope === null || source.contract.revision !== null)
49
49
  return false;
50
50
  const scope = source.contract.scope;
51
- if (scope.proposedVia !== null || scope.acceptance.length === 0 || scope.touches.length === 0 || scope.terms.profileState !== "resolved" || store.scopeSealed(taskId))
51
+ if (quarantinedAuthor(scope.proposedVia) || scope.acceptance.length === 0 || scope.touches.length === 0 || scope.terms.profileState !== "resolved" || store.scopeSealed(taskId))
52
52
  return false;
53
53
  store.raw().prepare(`INSERT INTO plan_authorization(task_id,source_digest,scope_digest,mode_digest,signed_by,created_at,basis) VALUES (?,?,?,?,?,?,'mode')
54
54
  ON CONFLICT(task_id) DO UPDATE SET source_digest=excluded.source_digest,scope_digest=excluded.scope_digest,mode_digest=excluded.mode_digest,signed_by=excluded.signed_by,created_at=excluded.created_at,basis='mode'`)
@@ -72,7 +72,7 @@ function escalatedPosture(store, taskId) {
72
72
  export function sealFlowFilingUnderGrant(store, taskId, grant, card, now) {
73
73
  const ref = store.lookupRef(taskId);
74
74
  const scope = store.getScope(taskId);
75
- if (ref === null || scope === null || scope.proposedVia != null || scope.profileState !== "resolved" || escalatedPosture(store, taskId))
75
+ if (ref === null || scope === null || quarantinedAuthor(scope.proposedVia) || scope.profileState !== "resolved" || escalatedPosture(store, taskId))
76
76
  return false;
77
77
  if (!unattendedGrantStands(store, grant.digest, grant.signedBy, ref.repo, scope.touches, now))
78
78
  return false;
@@ -81,7 +81,7 @@ export type PlannerContractScope = ContractTerms & {
81
81
  approvedDigest: string | null;
82
82
  approvedBy: string | null;
83
83
  };
84
- proposedVia: "mate" | "coordinator" | "scout" | null;
84
+ proposedVia: import("./scope.js").ScopeAuthor | null;
85
85
  };
86
86
  export type PlannerContract = {
87
87
  /** null: no scope was filed — the title is the only filed intent, and
@@ -96,7 +96,7 @@ function checkAccessHtml(view) {
96
96
  const setting = unavailable !== null && saved === "sign-ins" ? "sandboxed" : saved;
97
97
  const network = view.network?.setting ?? null;
98
98
  const names = { sandboxed: "Sandboxed", "sign-ins": "Sandboxed, with model and GitHub sign-ins", unsandboxed: "Not sandboxed" };
99
- const meaning = { sandboxed: null, "sign-ins": "Checks can read your Claude, Codex and GitHub sign-ins, and on a Mac your Keychain (any item every program may read).", unsandboxed: "Checks run with your full access to this machine and to Toolroll." };
99
+ const meaning = { sandboxed: null, "sign-ins": "Checks can read your Claude, Codex and GitHub sign-ins.", unsandboxed: "Checks run with your full access to this machine and to Toolroll." };
100
100
  // The sign-ins never widen the network: how many hosts they'd still need, and where to add them (a change of its own).
101
101
  const missing = setting === "sign-ins" && network !== null ? signInHostsMissing(network) : [];
102
102
  const hosts = missing.length === 0 ? "" : html `<p class="meta" data-sign-in-hosts>${missing.length} sign-in ${missing.length === 1 ? "host isn't" : "hosts aren't"} allowed yet · <a href="#network">Add them</a></p><details class="hosts"><summary>Show hosts</summary><p class="tool-name">${missing.join(", ")}</p></details>`;
@@ -22,6 +22,7 @@
22
22
  import { toolRefusal } from "./policy.js";
23
23
  import { createHash } from "node:crypto";
24
24
  import { spawn } from "node:child_process";
25
+ import { isProtectedEnvName } from "./protected-storage.js";
25
26
  import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
26
27
  import { homedir, tmpdir } from "node:os";
27
28
  import { realpathSync } from "node:fs";
@@ -572,7 +573,8 @@ function exchangeStdio(spec, values, timeoutMs, base, omit, request, fence) {
572
573
  let child;
573
574
  try {
574
575
  const launch = confined(spec.command, spec.args, { ...(fence ?? {}), temp: env["TMPDIR"] });
575
- child = spawn(launch.file, launch.args, { env: { ...env, ...launch.env }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
576
+ // A fenced server gets no chat or vault variable (protected-storage.ts) but the tool's own secrets, set by name.
577
+ child = spawn(launch.file, launch.args, { env: fence === null ? { ...env, ...launch.env } : { ...Object.fromEntries(Object.entries(env).filter(([name]) => !isProtectedEnvName(name) || Object.hasOwn(values, name))), ...launch.env }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
576
578
  }
577
579
  catch (error) {
578
580
  if (error instanceof ContainmentRefusal) {
package/dist/proof.js CHANGED
@@ -620,6 +620,8 @@ function verificationFailureWords(failure) {
620
620
  return "the retried verification command timed out after automatic recovery";
621
621
  case "custody-lost":
622
622
  return "automatic recovery stopped because this worker no longer owned the build";
623
+ case "coverage-incomplete":
624
+ return "the release check didn't cover everything it owed, or a required test went missing or was skipped";
623
625
  case "timed-out":
624
626
  return "the approved verification command timed out before checks finished";
625
627
  case "spawn-failed":
@@ -0,0 +1,67 @@
1
+ /** One app's storage, relative to the home folder, per platform. */
2
+ export type ProtectedApp = {
3
+ app: string;
4
+ darwin: readonly string[];
5
+ linux: readonly string[];
6
+ };
7
+ /**
8
+ * Every listed root. Checked against a real Mac (2026-10-25): existence only, never a file's contents. The 1Password
9
+ * containers and the extra Chrome, Edge and other browsers' folders below were found there and added.
10
+ */
11
+ export declare const PROTECTED_STORAGE: readonly ProtectedApp[];
12
+ /** The chat and vault variables no launch gets. */
13
+ export declare const PROTECTED_ENV_PREFIXES: readonly string[];
14
+ export declare function isProtectedEnvName(name: string): boolean;
15
+ /** `env` without a chat or vault variable. */
16
+ export declare function withoutProtectedEnv<T extends Record<string, string | undefined>>(env: T): T;
17
+ /** How far the link walk goes in each present root. */
18
+ export declare const PROTECTED_WALK: {
19
+ readonly depth: 4;
20
+ readonly entries: 5000;
21
+ };
22
+ /** The roots by name, under the (real) home folder and the overrides the plane's environment names. */
23
+ export declare function protectedStorageNames(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform): string[];
24
+ /**
25
+ * Every protected path one launch denies: each root by name and by real path, every link inside a present root that
26
+ * leads outward (where it leads, by real path and as spelled), and the overrides. Resolved at each launch, never kept.
27
+ * A link macOS makes in a sandboxed app's container to the same place under the home folder (its Desktop, Downloads,
28
+ * Fonts…) is the system's own mirror of the person's folders, not the app's data, and isn't followed (systemMirror).
29
+ *
30
+ * On a Mac, listing inside another app's container waits for the person to answer macOS's "access data from other apps"
31
+ * prompt when the process asking has no answer yet (a plane run by launchd: every launch, and the console with it, hung
32
+ * there in release check 3249). So the containers are walked only after a time-limited look in a child process
33
+ * (`containers`, containersListable) shows they can be listed without waiting. A look that fails any other way refuses
34
+ * the launch (it throws). When macOS is waiting on the person, the containers stay denied by name and real path, and
35
+ * the outward links the last complete walk found (kept in the plane's own state folder, linkCacheFile) stay denied too;
36
+ * with none kept, only the roots are denied, and that is said once.
37
+ */
38
+ export declare function protectedStorageDenies(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, say?: (line: string) => void, containers?: (roots: readonly string[]) => boolean, list?: (path: string) => import("node:fs").Dirent[]): string[];
39
+ /** How long the look at other apps' containers may take before Toolroll takes it that macOS is waiting on the person. */
40
+ export declare const CONTAINER_LOOK_MS = 3000;
41
+ /** What a look at the containers found: listable without waiting, macOS waiting on the person (the look timed out), or
42
+ * the look itself failing (it couldn't start, crashed or was killed by anything but its own limit). */
43
+ export type ContainerLook = "listable" | "waiting" | "failed";
44
+ /**
45
+ * Whether this process can list inside these app containers without macOS stopping to ask the person: the walk run
46
+ * first in a child process that is killed at CONTAINER_LOOK_MS. Finishing in time (listed, or refused outright) means
47
+ * the walk can run here too; only running into that limit means macOS is waiting on the person (only the child waited).
48
+ * A look that fails any other way is tried once more, then throws: the launch is refused rather than fenced with less.
49
+ * A yes is kept (macOS keeps the person's answer); a no is looked at again after CONTAINER_RELOOK_MS.
50
+ */
51
+ export declare function containersListable(roots: readonly string[], look?: (roots: readonly string[]) => ContainerLook, now?: () => number): boolean;
52
+ /** A look's outcome from its child's result: only its own limit (ETIMEDOUT) is macOS waiting; any other error, signal
53
+ * or exit code is a failed look. */
54
+ export declare function lookOutcome(result: {
55
+ error?: Error;
56
+ signal: NodeJS.Signals | null;
57
+ status: number | null;
58
+ }): ContainerLook;
59
+ /** Where the outward links the last complete container walk found are kept: the plane's own state folder (fenced from
60
+ * every build), one file per user. */
61
+ export declare function linkCacheFile(home: string, env?: Readonly<Record<string, string | undefined>>): string;
62
+ /** The kept links for this home: only a plain file of this user's (no link), at most LINK_CACHE_MAX, of exactly the
63
+ * kept shape; anything else is no cache at all. */
64
+ export declare function readLinkCache(file: string, home: string): string[] | null;
65
+ /** Keep a complete walk's links: written aside (owner-only) and renamed into place, only into a state folder that is
66
+ * already there. A failed write keeps whatever was kept before. */
67
+ export declare function writeLinkCache(file: string, home: string, targets: readonly string[]): void;