toolroll 0.9.67 → 0.9.69

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 (250) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/acp-client.js +1 -3
  3. package/dist/agent-board.d.ts +0 -1
  4. package/dist/agent-board.js +0 -1
  5. package/dist/agent-fence.d.ts +58 -0
  6. package/dist/agent-fence.js +214 -19
  7. package/dist/agentconfig.d.ts +1 -1
  8. package/dist/agentconfig.js +6 -2
  9. package/dist/batch-checks.js +14 -26
  10. package/dist/browser/workspace.css +1 -1
  11. package/dist/browser/workspace.js +31 -29
  12. package/dist/builder.d.ts +9 -0
  13. package/dist/builder.js +30 -27
  14. package/dist/canonical-json.d.ts +14 -0
  15. package/dist/canonical-json.js +36 -0
  16. package/dist/chat-actions.js +1 -2
  17. package/dist/chat-admission.d.ts +19 -0
  18. package/dist/chat-admission.js +22 -0
  19. package/dist/chat-channel.js +1 -1
  20. package/dist/chat-delivery.js +0 -1
  21. package/dist/chat-follow.d.ts +17 -0
  22. package/dist/chat-follow.js +32 -0
  23. package/dist/chat-messages.js +0 -1
  24. package/dist/chat-settings-page.d.ts +23 -0
  25. package/dist/chat-settings-page.js +41 -0
  26. package/dist/check-access.d.ts +54 -0
  27. package/dist/check-access.js +80 -0
  28. package/dist/claim.js +3 -6
  29. package/dist/claude-print.d.ts +12 -0
  30. package/dist/claude-print.js +34 -0
  31. package/dist/cli-http.d.ts +0 -7
  32. package/dist/cli-http.js +0 -21
  33. package/dist/cli.d.ts +3 -10
  34. package/dist/cli.js +15 -24
  35. package/dist/coding-provider.js +2 -1
  36. package/dist/coding-workspace.js +1 -1
  37. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  38. package/dist/connect-cli.js +331 -0
  39. package/dist/containment.d.ts +0 -2
  40. package/dist/containment.js +2 -6
  41. package/dist/contracts/chat-actions.d.ts +0 -1
  42. package/dist/contracts/chat-content.d.ts +0 -4
  43. package/dist/contracts/chat-content.js +0 -2
  44. package/dist/contracts/chat-tables.d.ts +0 -1
  45. package/dist/contracts/cli.d.ts +0 -62
  46. package/dist/contracts/cli.js +0 -4
  47. package/dist/contracts/coding-workspace.js +1 -1
  48. package/dist/contracts/console-api.d.ts +1 -0
  49. package/dist/contracts/console-api.js +1 -0
  50. package/dist/contracts/contract.js +1 -1
  51. package/dist/contracts/flow-card.d.ts +0 -1
  52. package/dist/contracts/flow-propose.d.ts +0 -1
  53. package/dist/contracts/flow-send.js +1 -1
  54. package/dist/contracts/gateway-tools.d.ts +0 -1
  55. package/dist/contracts/integration-metadata.js +1 -1
  56. package/dist/contracts/lead-tools.d.ts +0 -1
  57. package/dist/contracts/memory-pass.d.ts +0 -1
  58. package/dist/contracts/memory-pass.js +1 -1
  59. package/dist/contracts/plan.js +1 -1
  60. package/dist/contracts/project-knowledge.js +1 -1
  61. package/dist/contracts/project-skills.d.ts +0 -1
  62. package/dist/contracts/proof.js +1 -1
  63. package/dist/contracts/recipes.js +1 -1
  64. package/dist/contracts/scope.js +1 -1
  65. package/dist/contracts/scout-report.js +1 -1
  66. package/dist/contracts/teams-callback.d.ts +0 -1
  67. package/dist/converse.d.ts +0 -6
  68. package/dist/converse.js +0 -19
  69. package/dist/daemon.js +3 -2
  70. package/dist/decision.d.ts +1 -3
  71. package/dist/deprecations.d.ts +12 -4
  72. package/dist/deprecations.js +27 -4
  73. package/dist/desktop-update-recovery.js +1 -1
  74. package/dist/desktop-update.js +3 -2
  75. package/dist/discord-chat.js +4 -15
  76. package/dist/discord-settings.d.ts +2 -6
  77. package/dist/discord-settings.js +10 -38
  78. package/dist/discord.js +12 -23
  79. package/dist/evidence.d.ts +0 -1
  80. package/dist/evidence.js +0 -3
  81. package/dist/exec.d.ts +0 -9
  82. package/dist/exec.js +3 -81
  83. package/dist/flow-actions.js +0 -3
  84. package/dist/flow-draft.d.ts +1 -3
  85. package/dist/flow-draft.js +15 -28
  86. package/dist/flow-scripts.d.ts +1 -8
  87. package/dist/flow-scripts.js +0 -13
  88. package/dist/flow-send.js +1 -1
  89. package/dist/flow-share.d.ts +0 -2
  90. package/dist/flow-share.js +1 -6
  91. package/dist/flow-steps.js +1 -1
  92. package/dist/flow-triggers.d.ts +0 -1
  93. package/dist/flow-triggers.js +1 -2
  94. package/dist/flow-unattended.js +1 -12
  95. package/dist/flows.js +1 -1
  96. package/dist/health-cli.d.ts +2 -2
  97. package/dist/health-cli.js +1 -1
  98. package/dist/invoke.d.ts +39 -5
  99. package/dist/invoke.js +181 -24
  100. package/dist/is-record.d.ts +2 -0
  101. package/dist/is-record.js +2 -0
  102. package/dist/lead-doors.d.ts +1 -5
  103. package/dist/lead-doors.js +0 -4
  104. package/dist/lead-tools.js +3 -6
  105. package/dist/live-detail.d.ts +0 -1
  106. package/dist/live-detail.js +0 -6
  107. package/dist/live-terminal.js +2 -1
  108. package/dist/live.d.ts +0 -1
  109. package/dist/live.js +1 -2
  110. package/dist/mcp-core.js +0 -2
  111. package/dist/memory-pass.js +1 -1
  112. package/dist/model-catalog.js +1 -1
  113. package/dist/modes.js +1 -12
  114. package/dist/operate.d.ts +4 -0
  115. package/dist/operate.js +456 -750
  116. package/dist/password-bearer.d.ts +0 -2
  117. package/dist/password-bearer.js +0 -4
  118. package/dist/peek-cli.js +2 -1
  119. package/dist/peek.d.ts +2 -26
  120. package/dist/peek.js +2 -168
  121. package/dist/phase-routing.d.ts +0 -4
  122. package/dist/phase-routing.js +2 -21
  123. package/dist/planner-source.d.ts +2 -1
  124. package/dist/planner-source.js +2 -12
  125. package/dist/planner.js +2 -14
  126. package/dist/process-tree.d.ts +0 -2
  127. package/dist/process-tree.js +0 -10
  128. package/dist/project-delete-ui.d.ts +16 -0
  129. package/dist/project-delete-ui.js +60 -1
  130. package/dist/project-delete.d.ts +0 -2
  131. package/dist/project-delete.js +1 -3
  132. package/dist/project-learning.d.ts +0 -14
  133. package/dist/project-skills.d.ts +0 -1
  134. package/dist/project-skills.js +0 -14
  135. package/dist/project-tools.js +2 -10
  136. package/dist/provider.d.ts +0 -17
  137. package/dist/provider.js +0 -26
  138. package/dist/pulls.d.ts +0 -2
  139. package/dist/pulls.js +0 -9
  140. package/dist/release-adapters.d.ts +3 -0
  141. package/dist/release-adapters.js +15 -5
  142. package/dist/remote-exec.d.ts +3 -2
  143. package/dist/remote-exec.js +3 -2
  144. package/dist/repos.d.ts +0 -5
  145. package/dist/repos.js +2 -10
  146. package/dist/repository-context.d.ts +0 -2
  147. package/dist/repository-context.js +0 -4
  148. package/dist/restart-certification.d.ts +0 -2
  149. package/dist/restart-certification.js +1 -5
  150. package/dist/result-follow-ups.d.ts +1 -4
  151. package/dist/result-follow-ups.js +11 -21
  152. package/dist/result-review.d.ts +0 -2
  153. package/dist/result-review.js +0 -2
  154. package/dist/retention.d.ts +0 -1
  155. package/dist/retention.js +0 -1
  156. package/dist/review-context.d.ts +0 -1
  157. package/dist/review-context.js +1 -12
  158. package/dist/reviewer.d.ts +0 -16
  159. package/dist/reviewer.js +0 -18
  160. package/dist/run-closeout.js +2 -1
  161. package/dist/scope.d.ts +9 -0
  162. package/dist/scope.js +13 -18
  163. package/dist/scout-browser.js +2 -1
  164. package/dist/serve.js +11 -2
  165. package/dist/server/console.css +5 -133
  166. package/dist/server/flows.js +2 -2
  167. package/dist/server/http.d.ts +4 -0
  168. package/dist/server/http.js +5 -0
  169. package/dist/server/pages.js +2 -2
  170. package/dist/server/people-tokens.js +3 -4
  171. package/dist/server/remote.js +1 -6
  172. package/dist/server/render-chat.d.ts +9 -2
  173. package/dist/server/render-chat.js +53 -7
  174. package/dist/server/render-results.d.ts +3 -6
  175. package/dist/server/render-results.js +4 -9
  176. package/dist/server/render-tasks.js +6 -6
  177. package/dist/server/route-table.d.ts +0 -4
  178. package/dist/server/route-table.js +1 -6
  179. package/dist/server/settings.js +60 -23
  180. package/dist/server/task-views.js +1 -1
  181. package/dist/server/tasks.js +5 -3
  182. package/dist/setup-guide.d.ts +2 -2
  183. package/dist/skills.d.ts +0 -6
  184. package/dist/skills.js +0 -8
  185. package/dist/slack-chat.d.ts +1 -7
  186. package/dist/slack-chat.js +5 -18
  187. package/dist/slack-settings.d.ts +2 -6
  188. package/dist/slack-settings.js +10 -38
  189. package/dist/slack.js +12 -23
  190. package/dist/sleep.d.ts +2 -0
  191. package/dist/sleep.js +2 -0
  192. package/dist/store.d.ts +29 -75
  193. package/dist/store.js +89 -180
  194. package/dist/structured-output.d.ts +0 -1
  195. package/dist/structured-output.js +0 -1
  196. package/dist/subagent-work.d.ts +0 -2
  197. package/dist/subagent-work.js +0 -2
  198. package/dist/subagents.d.ts +1 -3
  199. package/dist/subagents.js +20 -33
  200. package/dist/subscription-chat.d.ts +1 -3
  201. package/dist/subscription-chat.js +2 -3
  202. package/dist/surface.js +2 -1
  203. package/dist/task-sizing.d.ts +1 -4
  204. package/dist/task-sizing.js +3 -10
  205. package/dist/teams-chat.js +4 -11
  206. package/dist/teams.js +12 -19
  207. package/dist/telegram-lead.js +2 -2
  208. package/dist/telegram.js +1 -1
  209. package/dist/token-notices.d.ts +0 -11
  210. package/dist/token-notices.js +1 -5
  211. package/dist/tool-secret-store.d.ts +0 -3
  212. package/dist/tool-secret-store.js +2 -11
  213. package/dist/toolroll-update.d.ts +0 -2
  214. package/dist/toolroll-update.js +2 -2
  215. package/dist/verification-evidence.d.ts +0 -1
  216. package/dist/verification-evidence.js +0 -1
  217. package/dist/webhooks.d.ts +0 -1
  218. package/dist/webhooks.js +0 -1
  219. package/dist/workspace-ui.d.ts +0 -8
  220. package/dist/workspace-ui.js +1 -6
  221. package/package.json +1 -1
  222. package/dist/contracts/native-census.d.ts +0 -42
  223. package/dist/contracts/native-census.js +0 -36
  224. package/dist/process-recovery-anchor.d.ts +0 -75
  225. package/dist/process-recovery-anchor.js +0 -188
  226. package/dist/process-recovery-coalition.d.ts +0 -25
  227. package/dist/process-recovery-coalition.js +0 -105
  228. package/dist/process-recovery-eligibility.d.ts +0 -179
  229. package/dist/process-recovery-eligibility.js +0 -174
  230. package/dist/process-recovery-native.d.ts +0 -161
  231. package/dist/process-recovery-native.js +0 -595
  232. package/dist/process-recovery-provenance.d.ts +0 -163
  233. package/dist/process-recovery-provenance.js +0 -292
  234. package/dist/process-recovery-services.d.ts +0 -56
  235. package/dist/process-recovery-services.js +0 -191
  236. package/dist/process-recovery-settlement.d.ts +0 -32
  237. package/dist/process-recovery-settlement.js +0 -133
  238. package/dist/process-recovery.d.ts +0 -41
  239. package/dist/process-recovery.js +0 -91
  240. package/dist/session-brief.d.ts +0 -8
  241. package/dist/session-brief.js +0 -24
  242. package/dist/session-cli.d.ts +0 -42
  243. package/dist/session-cli.js +0 -271
  244. package/dist/session-contract.d.ts +0 -183
  245. package/dist/session-contract.js +0 -122
  246. package/dist/session-remote.d.ts +0 -19
  247. package/dist/session-remote.js +0 -86
  248. package/dist/session-service.d.ts +0 -22
  249. package/dist/session-service.js +0 -157
  250. package/dist/team-cli.js +0 -692
package/dist/builder.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type NetworkProfile } from "./egress.js";
2
+ import { type CheckAccess } from "./check-access.js";
1
3
  import type { WritesMode } from "./writes-mode.js";
2
4
  import type { LaunchNetwork } from "./invoke.js";
3
5
  import type { ProviderId } from "./contracts/provider.js";
@@ -187,6 +189,13 @@ export declare function redactSecretAssignments(text: string): string;
187
189
  /** A build's network (egress.ts): the one its approval sealed, or — sealed before network policies existed — the
188
190
  * narrowed legacy one (provider hosts and the repository's registries, nothing added since). */
189
191
  export declare function launchNetworkOf(store: Store, taskRef: number, profile: ExecutionProfile): LaunchNetwork;
192
+ /** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
193
+ * run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
194
+ * approval sealed less its agent's own provider hosts (never its checks'). */
195
+ export declare function checkLaunchOf(store: Store, runId: number, profile: ExecutionProfile, platform?: NodeJS.Platform): {
196
+ access: CheckAccess;
197
+ network: NetworkProfile | null;
198
+ };
190
199
  /** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
191
200
  * its fence allows, as it was approved. */
192
201
  export declare function launchWritesOf(profile: ExecutionProfile): WritesMode;
package/dist/builder.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { legacyNetwork, repositoryEcosystems } from "./egress.js";
2
+ import { CHECK_ACCESS_DEFAULT, checkAccessHere, checkNetworkOf } from "./check-access.js";
2
3
  import { checkoutGit } from "./teardown-git.js";
3
4
  import { isAcpProvider } from "./acp-agents.js";
4
5
  import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
@@ -74,7 +75,7 @@ import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot
74
75
  import { attemptKeyOf, closeRunQuestions, noteOpenTool, recordAgentTurn } from "./agent-board.js";
75
76
  import { openStatusFile, pulseAgentBoard, removeStatusFile, statusBrief } from "./agent-pulse.js";
76
77
  import { withAgentTurns } from "./agent-turns.js";
77
- import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, artifactManifestOnly, sameDiffStatFacts } from "./proof.js";
78
+ import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, sameDiffStatFacts } from "./proof.js";
78
79
  import { parseHandoffArtifact } from "./contracts/handoff.js";
79
80
  import { captureReviewContext } from "./review-context.js";
80
81
  import { authoritySnapshotDigest, classifyRevisionAuthority, isMilestoneRegression, milestonesOf, parseExecutionPlanDocument, parsePlanRevisionProposal, parseProgressSnapshot, renderExecutionPlanDocument, PROGRESS_LIMITS, REVISION_LIMITS, } from "./plan.js";
@@ -268,7 +269,26 @@ function buildCheckoutGit(given, worktree, mainCheckout = null) {
268
269
  /** A run's setup and check commands, in its build's fence, writing where the approval this build runs under sealed
269
270
  * (never the scope as it may since have been rewritten; invoke.ts fencedCommands). */
270
271
  function commandRunner(store, runId, profile, inner) {
271
- return fencedCommands(store, runId, launchWritesOf(profile), inner);
272
+ // How far its checks reach (check-access.ts): what its approval sealed, stamped on the run (again: the dispatch stamped
273
+ // it first) so a later check of its result never runs wider; under sign-ins, behind its checks' own network.
274
+ const { access, network } = checkLaunchOf(store, runId, profile);
275
+ store.stampRunCheckAccess(runId, access, network);
276
+ return fencedCommands(store, runId, launchWritesOf(profile), inner, undefined, access, network ?? undefined);
277
+ }
278
+ /** A build's checks' level and network: its approval's level as this platform can give it (sign-ins sealed before
279
+ * run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
280
+ * approval sealed less its agent's own provider hosts (never its checks'). */
281
+ export function checkLaunchOf(store, runId, profile, platform = process.platform) {
282
+ const access = checkAccessHere(profile.checkAccess ?? CHECK_ACCESS_DEFAULT, platform);
283
+ if (access !== "sign-ins")
284
+ return { access, network: null };
285
+ const taskRef = store.getRun(runId)?.taskRef;
286
+ const sealed = taskRef === undefined ? undefined : launchNetworkOf(store, taskRef, profile);
287
+ // No sealed network to narrow from: its checks run sandboxed.
288
+ if (sealed === undefined || !("policy" in sealed))
289
+ return { access: CHECK_ACCESS_DEFAULT, network: null };
290
+ const repo = store.refForId(taskRef)?.repo ?? null;
291
+ return { access, network: checkNetworkOf(sealed.policy, profile.provider, repo === null ? { mode: "allowlist", hosts: [], loopback: "proxy-only" } : store.projectNetwork(repo)) };
272
292
  }
273
293
  /** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
274
294
  * its fence allows, as it was approved. */
@@ -369,7 +389,7 @@ export function proveApprovedProfile(scope, given) {
369
389
  * that forgot one is exactly the caller this is protecting against.
370
390
  */
371
391
  export async function build(store, request) {
372
- const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", model, maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git: givenGit = run, } = request;
392
+ const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git: givenGit = run, } = request;
373
393
  // The plane's own git on this build's checkout runs hardened, and never where the checkout's config names a command.
374
394
  const git = buildCheckoutGit(givenGit, worktree, store.getWorktree(worktree)?.repo ?? null);
375
395
  const scope = store.getScope(taskId);
@@ -422,6 +442,12 @@ export async function build(store, request) {
422
442
  // resume below can match on them honestly.
423
443
  const provenScopeDigest = scope?.approvedDigest ?? "";
424
444
  const provenProfileDigest = profileDigestOf(effective.profile);
445
+ // Its checks' level and network, stamped now with the proof: a batched result that runs no command here still
446
+ // carries what its approval allows its checks (check-access.ts).
447
+ {
448
+ const launch = checkLaunchOf(store, request.runId, effective.profile);
449
+ store.stampRunCheckAccess(request.runId, launch.access, launch.network);
450
+ }
425
451
  store.stampRun(request.runId, {
426
452
  scopeDigest: provenScopeDigest,
427
453
  profileDigest: provenProfileDigest,
@@ -1527,7 +1553,7 @@ export async function settleProviderOutcome(captured, result) {
1527
1553
  }
1528
1554
  catch { /* The commit gate still excludes it. */ }
1529
1555
  }
1530
- const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, proof, clock } = captured;
1556
+ const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, clock } = captured;
1531
1557
  // THE STOP FENCE after the provider (v52): a stop recorded while the
1532
1558
  // agent ran ends the attempt HERE, before any handoff is read, any park
1533
1559
  // sealed, or any commit made — whatever the process wrote is preserved
@@ -3534,29 +3560,6 @@ async function stripEvidenceImages(git, worktree) {
3534
3560
  const rest = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--quiet"], { cwd: worktree });
3535
3561
  return { ok: true, notice: evidenceImagesNotice(images), staged: rest.code !== 0 };
3536
3562
  }
3537
- /**
3538
- * `claude --output-format json` returns an envelope: the result is the
3539
- * summary, and the session id is what lets a malformed park be repaired by
3540
- * resuming the conversation that produced it instead of paying for a new one.
3541
- */
3542
- function envelope(stdout) {
3543
- try {
3544
- const parsed = JSON.parse(stdout);
3545
- return {
3546
- summary: typeof parsed.result === "string" && parsed.result.trim() !== ""
3547
- ? parsed.result.trim()
3548
- : "unattended build",
3549
- ...(typeof parsed.session_id === "string" && parsed.session_id !== ""
3550
- ? { sessionId: parsed.session_id }
3551
- : {}),
3552
- };
3553
- }
3554
- catch {
3555
- // An agent that printed something unparseable still did work — and a
3556
- // session nobody can name simply cannot be resumed.
3557
- return { summary: "unattended build" };
3558
- }
3559
- }
3560
3563
  /**
3561
3564
  * What a non-zero agent exit means, in words a person can act on: the
3562
3565
  * harness's own result line names the ending (a turn ceiling, an error
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
3
+ * function may change its output for any input. The two differ on purpose and must stay distinct:
4
+ *
5
+ * - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
6
+ * value (an undefined array element therefore renders as nothing: `[,1]`).
7
+ * - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
8
+ * `null` — the planner contract's bytes.
9
+ *
10
+ * Both sort object keys by UTF-16 code units and keep arrays in order.
11
+ */
12
+ export declare function canonicalJson(value: unknown): string;
13
+ /** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
14
+ export declare function canonicalJsonUndefinedAsNull(value: unknown): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
3
+ * function may change its output for any input. The two differ on purpose and must stay distinct:
4
+ *
5
+ * - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
6
+ * value (an undefined array element therefore renders as nothing: `[,1]`).
7
+ * - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
8
+ * `null` — the planner contract's bytes.
9
+ *
10
+ * Both sort object keys by UTF-16 code units and keep arrays in order.
11
+ */
12
+ export function canonicalJson(value) {
13
+ if (Array.isArray(value))
14
+ return `[${value.map(canonicalJson).join(",")}]`;
15
+ if (value !== null && typeof value === "object") {
16
+ const entries = Object.entries(value)
17
+ .filter(([, v]) => v !== undefined)
18
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
19
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
20
+ return `{${entries.join(",")}}`;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ /** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
25
+ export function canonicalJsonUndefinedAsNull(value) {
26
+ if (Array.isArray(value))
27
+ return `[${value.map(canonicalJsonUndefinedAsNull).join(",")}]`;
28
+ if (value !== null && typeof value === "object") {
29
+ const body = value;
30
+ return `{${Object.keys(body)
31
+ .sort()
32
+ .map(key => `${JSON.stringify(key)}:${canonicalJsonUndefinedAsNull(body[key])}`)
33
+ .join(",")}}`;
34
+ }
35
+ return JSON.stringify(value) ?? "null";
36
+ }
@@ -16,7 +16,6 @@ import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-kno
16
16
  import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
17
17
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
18
18
  import { readAuthModeStrict } from "./keys.js";
19
- import { parseProof } from "./proof.js";
20
19
  import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
21
20
  import { getDecision, recordDecision, retireDecision } from "./project-memory.js";
22
21
  import { resumeTaskStop, taskControlOf } from "./task-control.js";
@@ -26,7 +25,7 @@ import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
26
25
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
27
26
  import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
28
27
  import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
29
- import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, scheduleFromWords, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
28
+ import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
30
29
  import { describeSchedule, parseSchedule } from "./flow-schedule.js";
31
30
  import { dirname } from "node:path";
32
31
  import { handleOf, parseSoul, SOUL_CHARS, SUBAGENT_TEMPLATES, subagentLabel } from "./subagents.js";
@@ -0,0 +1,19 @@
1
+ import type { ChatBinding, ChatState } from "./chat-delivery-state.js";
2
+ import type { ChatEventBody } from "./contracts/chat-content.js";
3
+ import type { ChatApp } from "./contracts/flow.js";
4
+ /**
5
+ * Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
6
+ * A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
7
+ * that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
8
+ * A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
9
+ * member in their own conversation or a followed room. Null when it is not taken.
10
+ */
11
+ export declare function admitChatEvent(state: ChatState, app: ChatApp, installation: string, event: {
12
+ kind: "message" | "pair" | "action";
13
+ member: string;
14
+ channel: string;
15
+ isRoom: boolean;
16
+ payload: ChatEventBody;
17
+ }): {
18
+ binding: ChatBinding | null;
19
+ } | null;
@@ -0,0 +1,22 @@
1
+ import { channelInbox } from "./chat-inbox.js";
2
+ import { roomCommand } from "./chat-rooms.js";
3
+ /**
4
+ * Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
5
+ * A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
6
+ * that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
7
+ * A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
8
+ * member in their own conversation or a followed room. Null when it is not taken.
9
+ */
10
+ export function admitChatEvent(state, app, installation, event) {
11
+ const { kind, member, channel, isRoom, payload } = event;
12
+ const words = "text" in payload ? payload.text ?? "" : "";
13
+ const inbox = isRoom && kind === "message" ? channelInbox(state.store, app, installation, channel, words) : { watched: false, command: false };
14
+ const roomish = isRoom && (state.room(installation, channel) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
15
+ if (isRoom && !roomish)
16
+ return null;
17
+ const binding = state.bindingFor(installation, member);
18
+ const open = inbox.watched && !inbox.command;
19
+ if (kind === "pair" ? !!binding || isRoom : !open && (!binding || !state.live(binding) || (binding.channel !== channel && !roomish)))
20
+ return null;
21
+ return { binding };
22
+ }
@@ -9,7 +9,7 @@ import { LEAD_TOOL_SCHEMAS } from "./lead-tools.js";
9
9
  import { verifyApproverStanding } from "./principal.js";
10
10
  import { canonicalProject } from "./project.js";
11
11
  import { phoneText, projectLabel } from "./telegram-status.js";
12
- import { CHAT_CONTROLS, chatControlHref, chatResultHref, isChatControl, } from "./chat-controls.js";
12
+ import { CHAT_CONTROLS, chatControlHref, isChatControl, } from "./chat-controls.js";
13
13
  import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
14
14
  export function channelRepos(store, approver, registry) {
15
15
  const seen = new Set();
@@ -23,7 +23,6 @@ import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneT
23
23
  import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
24
24
  import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
25
25
  import { isShotsKind, resultShotsFor } from "./result-shots.js";
26
- import { CHAT_APP_NAMES } from "./flow-triggers.js";
27
26
  import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
28
27
  import { mergePullRequest } from "./pull-request-flow.js";
29
28
  import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
@@ -0,0 +1,17 @@
1
+ /** A pause that ends early, and quietly, when `signal` aborts. */
2
+ export declare const abortablePause: (signal: AbortSignal) => (ms: number) => Promise<void>;
3
+ /**
4
+ * Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
5
+ * messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
6
+ * A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
7
+ */
8
+ export declare function workChatQueue(steps: {
9
+ same: () => boolean;
10
+ ready: () => boolean;
11
+ pause: (ms: number) => Promise<void>;
12
+ notifications: () => boolean;
13
+ process: () => Promise<unknown>;
14
+ rooms: () => Promise<unknown>;
15
+ deliver: () => Promise<unknown>;
16
+ notify: () => Promise<unknown>;
17
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ /** The loop every hosted chat connection (Slack, Discord, Teams) runs over its saved queue. */
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ /** A pause that ends early, and quietly, when `signal` aborts. */
4
+ export const abortablePause = (signal) => async (ms) => {
5
+ try {
6
+ await sleep(ms, undefined, { signal });
7
+ }
8
+ catch {
9
+ /* shutdown */
10
+ }
11
+ };
12
+ /**
13
+ * Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
14
+ * messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
15
+ * A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
16
+ */
17
+ export async function workChatQueue(steps) {
18
+ while (steps.same()) {
19
+ if (!steps.ready()) {
20
+ await steps.pause(1000);
21
+ continue;
22
+ }
23
+ await steps.process();
24
+ if (steps.same())
25
+ await steps.rooms();
26
+ if (steps.same())
27
+ await steps.deliver();
28
+ if (steps.same() && steps.notifications())
29
+ await steps.notify();
30
+ await steps.pause(1000);
31
+ }
32
+ }
@@ -8,7 +8,6 @@
8
8
  *
9
9
  * Imports the store's types only (no runtime cycle): the store builds its chats on it.
10
10
  */
11
- import { join } from "node:path";
12
11
  import { neverHeld, watchedPing } from "./agent-board.js";
13
12
  import { ChatCore } from "./chat-core.js";
14
13
  import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
@@ -0,0 +1,23 @@
1
+ import type { Store } from "./store.js";
2
+ import { type Html } from "./html.js";
3
+ import { type ChatIdentity } from "./chat-delivery-state.js";
4
+ export type ChatSettingsOptions = {
5
+ code?: string;
6
+ problem?: string;
7
+ now?: Date;
8
+ who?: string;
9
+ };
10
+ /** What differs between the Slack and Discord settings pages; everything else is one page. */
11
+ export type ChatSettingsPage = {
12
+ provider: "slack" | "discord";
13
+ label: string;
14
+ credentials: ChatIdentity | null;
15
+ /** How to create the app, shown before it is connected. */
16
+ setup: Html;
17
+ /** The connect form's fields around the password. */
18
+ connect: (password: Html) => Html;
19
+ /** The "Access and setup" paragraphs. */
20
+ access: Html;
21
+ };
22
+ /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
23
+ export declare function chatSettingsHtml(store: Store, dir: string, options: ChatSettingsOptions, page: ChatSettingsPage): Html;
@@ -0,0 +1,41 @@
1
+ import { loadPrimary } from "./webhooks.js";
2
+ import { html, postForm } from "./html.js";
3
+ import { ChatState } from "./chat-delivery-state.js";
4
+ /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
5
+ export function chatSettingsHtml(store, dir, options, page) {
6
+ const { provider, label, credentials } = page, state = new ChatState(store, provider), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
7
+ const runtime = credentials ? state.runtime(credentials.installation) : null;
8
+ const now = options.now ?? new Date(), live = runtime &&
9
+ runtime.leaseUntil !== null &&
10
+ runtime.leaseUntil > now.toISOString() &&
11
+ runtime.connected;
12
+ const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
13
+ const post = (action, content) => postForm(`/settings/${provider}/${action}`, content, { attrs: { class: "card" } });
14
+ const parts = [];
15
+ if (!credentials) {
16
+ parts.push(html `<p>Manage projects and review results in a private ${label} conversation.</p>`, page.setup, post("connect", page.connect(password)));
17
+ }
18
+ else {
19
+ parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
20
+ if (runtime?.problem)
21
+ parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
22
+ if (!live && !runtime?.problem)
23
+ parts.push(html `<p>Keep the Toolroll worker running to receive ${label} messages.</p>`);
24
+ if (options.code) {
25
+ parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll ${label} app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/${provider}">Check connection</a>`);
26
+ }
27
+ else if (binding && state.live(binding)) {
28
+ const { pending, dropped: failed } = state.partCounts(binding.id);
29
+ parts.push(html `<p>Your ${label} account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === provider
30
+ ? html `<p>Task updates are sent here.</p>`
31
+ : post("alerts", html `<button type="submit">Send task updates here</button>`));
32
+ }
33
+ else {
34
+ parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
35
+ }
36
+ if (binding && state.live(binding) && !options.code)
37
+ parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
38
+ parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect ${label}</button>`)}</details>`);
39
+ }
40
+ return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>${label}</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary>${page.access}</details></section>`;
41
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
3
+ * plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
4
+ * of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
5
+ * whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
6
+ * operator's act with a password typed now; any approver for the project may narrow it back.
7
+ */
8
+ import { type NetworkProfile, type ProjectNetwork } from "./egress.js";
9
+ export declare const CHECK_ACCESS_LEVELS: readonly ["sandboxed", "sign-ins", "unsandboxed"];
10
+ export type CheckAccess = typeof CHECK_ACCESS_LEVELS[number];
11
+ export declare const CHECK_ACCESS_DEFAULT: CheckAccess;
12
+ export declare const isCheckAccess: (value: unknown) => value is CheckAccess;
13
+ /** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
14
+ export declare const checkAccessOf: (value: unknown) => CheckAccess;
15
+ export declare const widerThan: (a: CheckAccess, b: CheckAccess) => boolean;
16
+ /** The narrowest of several levels (none given: the default). */
17
+ export declare const narrowestAccess: (levels: readonly CheckAccess[]) => CheckAccess;
18
+ /** The level as a person reads it, in a sentence's middle ("Checks: …"). */
19
+ export declare function checkAccessWords(level: CheckAccess): string;
20
+ /** What each wider level means, said before anyone chooses it. */
21
+ export declare const CHECK_ACCESS_WARNINGS: Readonly<Record<Exclude<CheckAccess, "sandboxed">, string>>;
22
+ /** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
23
+ export declare const SIGN_IN_HOSTS: readonly string[];
24
+ /** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
25
+ * for them: the operator adds them, as a network change of its own. */
26
+ export declare function signInHostsMissing(network: ProjectNetwork): string[];
27
+ /** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
28
+ * do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
29
+ export declare function narrowestNetwork(a: NetworkProfile, b: NetworkProfile): NetworkProfile;
30
+ /** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
31
+ * its checks'); narrower if a project host happens to be one of them. */
32
+ export declare function checkNetworkOf(sealed: NetworkProfile, provider: string, project: ProjectNetwork): NetworkProfile;
33
+ /** Off everywhere, for now (0.9.69): no `sign-ins`. Reading a sign-in file resolves its parent folders by path (Node
34
+ * has no openat), and each review round found another way a build or a check could swap a folder on that path between
35
+ * the checks and the read (on Linux a Claude build can write ~/.claude outright). Until that read is race-free, no
36
+ * check gets sign-ins: setting the level is refused, a level saved or sealed before (or copied in from another
37
+ * database) runs sandboxed, a scope seals sandboxed, and the sign-ins launch refuses first thing. The code behind it
38
+ * (signInGrants, readGrantedFile, the copies, the network fence) stays, guarded, for when such a reader exists; flip
39
+ * SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
40
+ export declare const SIGN_INS_READY = false;
41
+ export declare const signInsAvailable: (platform?: NodeJS.Platform) => boolean;
42
+ /** Why sign-ins can't be chosen here, said plainly (null: they can). */
43
+ export declare const signInsUnavailableWords: (platform?: NodeJS.Platform) => string | null;
44
+ /** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
45
+ * wider). Everything that seals, launches or stamps a check's level goes through this. */
46
+ export declare const checkAccessHere: (level: CheckAccess, platform?: NodeJS.Platform) => CheckAccess;
47
+ /** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
48
+ * on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
49
+ * sandboxed); otherwise the saved level as this platform can give it. */
50
+ export declare function checkAccessRunsAs(level: CheckAccess, here: {
51
+ platform: NodeJS.Platform;
52
+ fenced: boolean;
53
+ unconfinedAllowed: boolean;
54
+ }): CheckAccess;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Check access (v132, docs/plans/check-access.md): how far a project's setup and check commands reach beyond the
3
+ * plane's check fence. `sandboxed` (the default) is the fence as it is; `sign-ins` also reads the exact sign-in files
4
+ * of Claude, Codex and gh, under the project's network fence; `unsandboxed` runs them with no OS fence, trusted by the
5
+ * whole instance, for a project whose checks must create sandboxes themselves (Toolroll's own). Widening is an instance
6
+ * operator's act with a password typed now; any approver for the project may narrow it back.
7
+ */
8
+ import { PROVIDER_HOSTS } from "./egress.js";
9
+ export const CHECK_ACCESS_LEVELS = ["sandboxed", "sign-ins", "unsandboxed"];
10
+ export const CHECK_ACCESS_DEFAULT = "sandboxed";
11
+ export const isCheckAccess = (value) => typeof value === "string" && CHECK_ACCESS_LEVELS.includes(value);
12
+ /** A stored or sealed value as a level: anything missing or unreadable is the narrowest, never a wider one. */
13
+ export const checkAccessOf = (value) => isCheckAccess(value) ? value : CHECK_ACCESS_DEFAULT;
14
+ const RANK = { sandboxed: 0, "sign-ins": 1, unsandboxed: 2 };
15
+ export const widerThan = (a, b) => RANK[a] > RANK[b];
16
+ /** The narrowest of several levels (none given: the default). */
17
+ export const narrowestAccess = (levels) => levels.reduce((low, one) => RANK[one] < RANK[low] ? one : low, levels.length === 0 ? CHECK_ACCESS_DEFAULT : "unsandboxed");
18
+ /** The level as a person reads it, in a sentence's middle ("Checks: …"). */
19
+ export function checkAccessWords(level) {
20
+ return level === "sandboxed" ? "sandboxed" : level === "sign-ins" ? "sandboxed, with model and GitHub sign-ins" : "not sandboxed";
21
+ }
22
+ /** What each wider level means, said before anyone chooses it. */
23
+ export const CHECK_ACCESS_WARNINGS = {
24
+ "sign-ins": "This project's checks can read your Claude, Codex and GitHub sign-ins. Its tests could use or send those credentials anywhere its network setting allows. Only choose this for projects you trust.",
25
+ unsandboxed: "This project's checks get your full access to this machine and to Toolroll itself, including its database and every sign-in. Only for a project whose checks must create sandboxes themselves, such as Toolroll's own.",
26
+ };
27
+ /** The services whose sign-ins a `sign-ins` check reads: Claude's and Codex's own hosts (egress.ts), and GitHub's. */
28
+ export const SIGN_IN_HOSTS = [...PROVIDER_HOSTS["claude"], ...PROVIDER_HOSTS["codex"], "github.com", "api.github.com"];
29
+ /** Which of those a project's network doesn't allow (an open network allows all). The check's network is never widened
30
+ * for them: the operator adds them, as a network change of its own. */
31
+ export function signInHostsMissing(network) {
32
+ if (network.mode === "open")
33
+ return [];
34
+ const covered = (host) => network.hosts.some(one => one === host || (one.startsWith("*.") && host.endsWith(one.slice(1))));
35
+ return SIGN_IN_HOSTS.filter(host => !covered(host));
36
+ }
37
+ /** The narrower of two networks: every host both allow (a wildcard allows what it covers), local services only if both
38
+ * do, open only if both are. A check never reaches what either its sealed network or the project's current one refuses. */
39
+ export function narrowestNetwork(a, b) {
40
+ const loopback = a.loopback === "any" && b.loopback === "any" ? "any" : "proxy-only";
41
+ if (a.mode === "open" && b.mode === "open")
42
+ return { version: 1, mode: "open", hosts: [], loopback };
43
+ if (a.mode === "open")
44
+ return { ...b, loopback };
45
+ if (b.mode === "open")
46
+ return { ...a, loopback };
47
+ const covers = (list, host) => list.some(one => one === host || (one.startsWith("*.") && (host.endsWith(one.slice(1)) || host === one)));
48
+ const hosts = [...new Set([...a.hosts.filter(host => covers(b.hosts, host)), ...b.hosts.filter(host => covers(a.hosts, host))])].sort();
49
+ return { version: 1, mode: "allowlist", hosts, loopback };
50
+ }
51
+ /** A build's checks' own network: what its approval sealed, less its agent's provider hosts (those are the agent's, never
52
+ * its checks'); narrower if a project host happens to be one of them. */
53
+ export function checkNetworkOf(sealed, provider, project) {
54
+ if (sealed.mode === "open")
55
+ return sealed;
56
+ // A provider host the project itself lists (an operator added it, for its sign-ins, say) is the checks' too.
57
+ const agent = new Set((PROVIDER_HOSTS[provider] ?? []).filter(host => !project.hosts.includes(host)));
58
+ return { ...sealed, hosts: sealed.hosts.filter(host => !agent.has(host)) };
59
+ }
60
+ /** Off everywhere, for now (0.9.69): no `sign-ins`. Reading a sign-in file resolves its parent folders by path (Node
61
+ * has no openat), and each review round found another way a build or a check could swap a folder on that path between
62
+ * the checks and the read (on Linux a Claude build can write ~/.claude outright). Until that read is race-free, no
63
+ * check gets sign-ins: setting the level is refused, a level saved or sealed before (or copied in from another
64
+ * database) runs sandboxed, a scope seals sandboxed, and the sign-ins launch refuses first thing. The code behind it
65
+ * (signInGrants, readGrantedFile, the copies, the network fence) stays, guarded, for when such a reader exists; flip
66
+ * SIGN_INS_READY then (Linux stays off until its copies are race-free too). */
67
+ export const SIGN_INS_READY = false;
68
+ export const signInsAvailable = (platform = process.platform) => SIGN_INS_READY && platform !== "linux";
69
+ /** Why sign-ins can't be chosen here, said plainly (null: they can). */
70
+ export const signInsUnavailableWords = (platform = process.platform) => signInsAvailable(platform) ? null
71
+ : "Not available yet: sign-ins can't be shared with checks safely. Choose Sandboxed or Not sandboxed.";
72
+ /** The level checks actually get on this platform: `sign-ins` where it isn't available is `sandboxed` (narrower, never
73
+ * wider). Everything that seals, launches or stamps a check's level goes through this. */
74
+ export const checkAccessHere = (level, platform = process.platform) => level === "sign-ins" && !signInsAvailable(platform) ? CHECK_ACCESS_DEFAULT : level;
75
+ /** How a project's checks actually run here, as the settings page and `project check-access` say it: with no OS fence
76
+ * on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
77
+ * sandboxed); otherwise the saved level as this platform can give it. */
78
+ export function checkAccessRunsAs(level, here) {
79
+ return !here.fenced && here.unconfinedAllowed ? "unsandboxed" : checkAccessHere(level, here.platform);
80
+ }
package/dist/claim.js CHANGED
@@ -749,7 +749,7 @@ export const MAX_STRIKES = 3;
749
749
  function sealInterrupted(store, leaseId, args) {
750
750
  const { runId, taskId, stopRun, now } = args;
751
751
  const db = store.handle;
752
- const run = openAttempt(store, leaseId, runId, "an interruption");
752
+ openAttempt(store, leaseId, runId, "an interruption");
753
753
  // A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
754
754
  const fenced = !releaseAs(store, leaseId, "interrupted", now);
755
755
  if (args.message !== undefined && args.message.trim() !== "") {
@@ -797,7 +797,7 @@ function sealInterrupted(store, leaseId, args) {
797
797
  function sealHandBack(store, leaseId, args) {
798
798
  const { runId, taskId, now } = args;
799
799
  const db = store.handle;
800
- const run = openAttempt(store, leaseId, runId, "a hand-back");
800
+ openAttempt(store, leaseId, runId, "a hand-back");
801
801
  // A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
802
802
  const fenced = !releaseAs(store, leaseId, "interrupted", now);
803
803
  if (args.message.trim() !== "")
@@ -974,7 +974,6 @@ const PLAN_BACKOFF_MS = [60_000, 2 * 60_000, 4 * 60_000];
974
974
  */
975
975
  function sealPlan(store, leaseId, args) {
976
976
  const { runId, taskId, plan, now } = args;
977
- const db = store.handle;
978
977
  // The ingestion record (contract handoff, task 1) is composed here from
979
978
  // the filed terms and the drafted plan; its file and row land inside the
980
979
  // transaction below, only after the stale-source check admits the draft.
@@ -1199,7 +1198,7 @@ function describeSourceDrift(source, current) {
1199
1198
  */
1200
1199
  function sealRevision(store, leaseId, args) {
1201
1200
  const { runId, taskId, taskRef, revision, now } = args;
1202
- const run = openAttempt(store, leaseId, runId, "a plan revision", "builder");
1201
+ openAttempt(store, leaseId, runId, "a plan revision", "builder");
1203
1202
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1204
1203
  if (stopped !== null)
1205
1204
  return { ok: false, reason: "stopped" };
@@ -1249,7 +1248,6 @@ function sealRevision(store, leaseId, args) {
1249
1248
  */
1250
1249
  function sealPlanFailure(store, leaseId, args) {
1251
1250
  const { runId, taskId, failure: kind, message, now } = args;
1252
- const db = store.handle;
1253
1251
  const run = openAttempt(store, leaseId, runId, "a failure", "planner");
1254
1252
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1255
1253
  if (stopped !== null)
@@ -1331,7 +1329,6 @@ function sealPlanFailure(store, leaseId, args) {
1331
1329
  */
1332
1330
  function sealScout(store, leaseId, args) {
1333
1331
  const { runId, taskId, report, now } = args;
1334
- const db = store.handle;
1335
1332
  const run = openAttempt(store, leaseId, runId, "a report", "scout");
1336
1333
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1337
1334
  if (stopped !== null)
@@ -0,0 +1,12 @@
1
+ import type { ExecResult, run } from "./exec.js";
2
+ export type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
3
+ /** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
4
+ export declare const CLAUDE_NO_TOOLS_ARGS: readonly string[];
5
+ /** `--model <model>`, or nothing for the CLI's own default. */
6
+ export declare const claudeModelArgs: (model: string) => string[];
7
+ /** What a failed call's output says when this computer's Claude isn't signed in. */
8
+ export declare const CLAUDE_SIGNED_OUT: RegExp;
9
+ /** `claude -p --output-format json` stdout as its one object, or null. */
10
+ export declare function claudeJsonBody(stdout: string, cap: number): Record<string, unknown> | null;
11
+ /** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
12
+ export declare function inScratchDir<T>(prefix: string, work: (dir: string) => Promise<T>): Promise<T>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The parts every bare `claude -p` call shares: a throwaway working folder, the flags that leave it with no tools,
3
+ * no MCP servers and no saved session, and reading its one JSON reply. Each caller still builds its own argv around
4
+ * these, in its own order.
5
+ */
6
+ import { Buffer } from "node:buffer";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { strictJsonParse } from "./converse.js";
11
+ /** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
12
+ export const CLAUDE_NO_TOOLS_ARGS = [
13
+ "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
14
+ "--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
15
+ ];
16
+ /** `--model <model>`, or nothing for the CLI's own default. */
17
+ export const claudeModelArgs = (model) => (model === "default" ? [] : ["--model", model]);
18
+ /** What a failed call's output says when this computer's Claude isn't signed in. */
19
+ export const CLAUDE_SIGNED_OUT = /not logged in|login|authenticat/i;
20
+ /** `claude -p --output-format json` stdout as its one object, or null. */
21
+ export function claudeJsonBody(stdout, cap) {
22
+ const parsed = strictJsonParse(Buffer.from(stdout, "utf8"), cap, 12);
23
+ return parsed.ok && typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : null;
24
+ }
25
+ /** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
26
+ export async function inScratchDir(prefix, work) {
27
+ const dir = mkdtempSync(join(tmpdir(), prefix));
28
+ try {
29
+ return await work(dir);
30
+ }
31
+ finally {
32
+ rmSync(dir, { recursive: true, force: true });
33
+ }
34
+ }