toolroll 0.9.93 → 0.9.95

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 (436) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +10 -0
  5. package/dist/agent-fence.js +6 -3
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -1
  12. package/dist/assignment.js +5 -0
  13. package/dist/attest.d.ts +3 -0
  14. package/dist/attest.js +10 -0
  15. package/dist/boot-identity.d.ts +2 -1
  16. package/dist/boot-identity.js +6 -4
  17. package/dist/bounded-spawn.d.ts +57 -0
  18. package/dist/bounded-spawn.js +220 -0
  19. package/dist/browser/terminal.js +42 -42
  20. package/dist/browser/workspace.css +1 -1
  21. package/dist/browser/workspace.js +32 -32
  22. package/dist/browser-crew.js +2 -0
  23. package/dist/browser-shell.d.ts +1 -1
  24. package/dist/browser-shell.js +2 -2
  25. package/dist/browser-workspace.d.ts +50 -2
  26. package/dist/browser-workspace.js +3 -3
  27. package/dist/build-review.js +2 -0
  28. package/dist/builder-restart.d.ts +21 -0
  29. package/dist/builder-restart.js +50 -0
  30. package/dist/builder.d.ts +11 -2
  31. package/dist/builder.js +234 -101
  32. package/dist/card-catalog.d.ts +209 -0
  33. package/dist/card-catalog.js +133 -0
  34. package/dist/card-cli.d.ts +43 -0
  35. package/dist/card-cli.js +44 -0
  36. package/dist/card-render.d.ts +26 -0
  37. package/dist/card-render.js +223 -0
  38. package/dist/card-sources.d.ts +68 -0
  39. package/dist/card-sources.js +234 -0
  40. package/dist/chat-acceptance.js +1 -1
  41. package/dist/chat-actions.d.ts +106 -9
  42. package/dist/chat-actions.js +473 -46
  43. package/dist/chat-approval.d.ts +13 -0
  44. package/dist/chat-approval.js +39 -9
  45. package/dist/chat-card-part.d.ts +11 -0
  46. package/dist/chat-card-part.js +18 -0
  47. package/dist/chat-card.d.ts +46 -0
  48. package/dist/chat-card.js +99 -0
  49. package/dist/chat-channel.d.ts +2 -0
  50. package/dist/chat-channel.js +37 -14
  51. package/dist/chat-controls.d.ts +4 -0
  52. package/dist/chat-controls.js +4 -0
  53. package/dist/chat-decide.d.ts +10 -16
  54. package/dist/chat-decide.js +79 -62
  55. package/dist/chat-delivery.js +13 -4
  56. package/dist/chat-flow.d.ts +0 -27
  57. package/dist/chat-flow.js +31 -2
  58. package/dist/chat-quiet.js +4 -4
  59. package/dist/chat-task-actions.d.ts +3 -2
  60. package/dist/chat-task-actions.js +19 -5
  61. package/dist/chat-terms.d.ts +14 -2
  62. package/dist/chat-terms.js +35 -0
  63. package/dist/chat-voice.d.ts +1 -1
  64. package/dist/chat-voice.js +2 -2
  65. package/dist/check-facts.d.ts +3 -1
  66. package/dist/check-facts.js +57 -11
  67. package/dist/check-failure.d.ts +24 -3
  68. package/dist/check-failure.js +89 -13
  69. package/dist/check-levels-ui.d.ts +4 -0
  70. package/dist/check-levels-ui.js +10 -1
  71. package/dist/check-load.d.ts +85 -0
  72. package/dist/check-load.js +147 -0
  73. package/dist/check-only-standing.d.ts +36 -0
  74. package/dist/check-only-standing.js +39 -0
  75. package/dist/check-only.d.ts +15 -0
  76. package/dist/check-only.js +99 -0
  77. package/dist/check-repair.d.ts +47 -0
  78. package/dist/check-repair.js +240 -0
  79. package/dist/check-report.d.ts +2 -0
  80. package/dist/check-report.js +1 -1
  81. package/dist/check-stamp.d.ts +46 -0
  82. package/dist/check-stamp.js +93 -0
  83. package/dist/checkout-cleanup.d.ts +36 -5
  84. package/dist/checkout-cleanup.js +166 -14
  85. package/dist/claim.d.ts +6 -0
  86. package/dist/claim.js +13 -2
  87. package/dist/cli-help.d.ts +4 -1
  88. package/dist/cli-help.js +49 -30
  89. package/dist/cli.js +15 -1
  90. package/dist/code-extract.d.ts +44 -0
  91. package/dist/code-extract.js +88 -0
  92. package/dist/code-map-acts.d.ts +24 -0
  93. package/dist/code-map-acts.js +53 -0
  94. package/dist/code-map-admission.d.ts +52 -0
  95. package/dist/code-map-admission.js +295 -0
  96. package/dist/code-map-build.d.ts +63 -0
  97. package/dist/code-map-build.js +415 -0
  98. package/dist/code-map-cli.d.ts +153 -0
  99. package/dist/code-map-cli.js +103 -0
  100. package/dist/code-map-main.d.ts +1 -0
  101. package/dist/code-map-main.js +48 -0
  102. package/dist/code-map-read.d.ts +106 -0
  103. package/dist/code-map-read.js +171 -0
  104. package/dist/code-map-report.d.ts +37 -0
  105. package/dist/code-map-report.js +95 -0
  106. package/dist/code-map-shape.d.ts +172 -0
  107. package/dist/code-map-shape.js +327 -0
  108. package/dist/code-map-store.d.ts +136 -0
  109. package/dist/code-map-store.js +233 -0
  110. package/dist/coding-provider.d.ts +3 -1
  111. package/dist/coding-provider.js +12 -7
  112. package/dist/coding-update.js +3 -2
  113. package/dist/coding-workspace.d.ts +1 -1
  114. package/dist/coding-workspace.js +8 -8
  115. package/dist/console-chrome.js +2 -2
  116. package/dist/containment.d.ts +5 -0
  117. package/dist/containment.js +14 -1
  118. package/dist/contracts/chat-actions.d.ts +92 -0
  119. package/dist/contracts/chat-actions.js +20 -0
  120. package/dist/contracts/chat-card.d.ts +91 -0
  121. package/dist/contracts/chat-card.js +54 -0
  122. package/dist/contracts/chat-content.d.ts +191 -0
  123. package/dist/contracts/chat-content.js +4 -0
  124. package/dist/contracts/cli.d.ts +52 -0
  125. package/dist/contracts/cli.js +4 -0
  126. package/dist/contracts/console-api.d.ts +12 -3
  127. package/dist/contracts/console-api.js +12 -3
  128. package/dist/contracts/flow-propose.d.ts +8 -1
  129. package/dist/contracts/flow-propose.js +3 -1
  130. package/dist/contracts/flow-send.d.ts +126 -0
  131. package/dist/contracts/flow-send.js +3 -0
  132. package/dist/contracts/gateway-tools.d.ts +17 -0
  133. package/dist/contracts/gateway-tools.js +3 -1
  134. package/dist/contracts/lead-tools.d.ts +234 -0
  135. package/dist/contracts/lead-tools.js +25 -1
  136. package/dist/contracts/project-knowledge.d.ts +38 -5
  137. package/dist/contracts/project-knowledge.js +11 -5
  138. package/dist/contracts/project-memory.d.ts +3 -1
  139. package/dist/contracts/project-memory.js +3 -2
  140. package/dist/contracts/storage.d.ts +14 -0
  141. package/dist/contracts/storage.js +3 -1
  142. package/dist/contracts/store-json.d.ts +18 -0
  143. package/dist/contracts/youiyoux-request.d.ts +1 -1
  144. package/dist/contracts/youiyoux-request.js +1 -1
  145. package/dist/desktop-access.js +6 -2
  146. package/dist/discord-chat.js +14 -0
  147. package/dist/dispatch.d.ts +1 -1
  148. package/dist/dispatch.js +25 -16
  149. package/dist/dispose.d.ts +19 -0
  150. package/dist/dispose.js +55 -9
  151. package/dist/easy-start.js +2 -3
  152. package/dist/eval-oracle.js +4 -1
  153. package/dist/evals.js +2 -1
  154. package/dist/evidence-pair.d.ts +53 -0
  155. package/dist/evidence-pair.js +128 -0
  156. package/dist/exhaustion.d.ts +17 -4
  157. package/dist/exhaustion.js +44 -4
  158. package/dist/export.js +1 -1
  159. package/dist/failing-first.d.ts +31 -0
  160. package/dist/failing-first.js +44 -0
  161. package/dist/fallback-schema.d.ts +8 -0
  162. package/dist/fallback-schema.js +40 -0
  163. package/dist/fallback.d.ts +138 -0
  164. package/dist/fallback.js +605 -0
  165. package/dist/fast-start.d.ts +136 -0
  166. package/dist/fast-start.js +431 -0
  167. package/dist/fast-terms.d.ts +9 -0
  168. package/dist/fast-terms.js +23 -0
  169. package/dist/fenced-check-coverage.d.ts +62 -0
  170. package/dist/fenced-check-coverage.js +54 -0
  171. package/dist/findings.d.ts +224 -0
  172. package/dist/findings.js +449 -0
  173. package/dist/first-run.js +1 -1
  174. package/dist/flaky-ci.d.ts +31 -0
  175. package/dist/flaky-ci.js +149 -0
  176. package/dist/flaky-cli.d.ts +60 -0
  177. package/dist/flaky-cli.js +60 -0
  178. package/dist/flaky.d.ts +75 -9
  179. package/dist/flaky.js +255 -0
  180. package/dist/fleet-schema.d.ts +17 -0
  181. package/dist/fleet-schema.js +135 -0
  182. package/dist/flow-browser.js +2 -2
  183. package/dist/flow-card-task.js +3 -0
  184. package/dist/flow-code.d.ts +2 -2
  185. package/dist/flow-code.js +9 -5
  186. package/dist/flow-result-card.d.ts +22 -0
  187. package/dist/flow-result-card.js +55 -0
  188. package/dist/flow-send.js +12 -9
  189. package/dist/flow-steps.js +1 -1
  190. package/dist/flow-triggers.d.ts +1 -1
  191. package/dist/flow-triggers.js +2 -2
  192. package/dist/flow-unattended.d.ts +26 -0
  193. package/dist/flow-unattended.js +12 -3
  194. package/dist/flows-ui.js +10 -2
  195. package/dist/follow-through-brief.d.ts +12 -0
  196. package/dist/follow-through-brief.js +31 -0
  197. package/dist/follow-through-cli.d.ts +23 -0
  198. package/dist/follow-through-cli.js +153 -0
  199. package/dist/git-incident.d.ts +16 -0
  200. package/dist/git-incident.js +69 -6
  201. package/dist/github-import.d.ts +62 -0
  202. package/dist/github-import.js +146 -0
  203. package/dist/guides.js +15 -10
  204. package/dist/handover-note.d.ts +33 -0
  205. package/dist/handover-note.js +106 -0
  206. package/dist/invoke.d.ts +25 -2
  207. package/dist/invoke.js +157 -58
  208. package/dist/job-object-helper.ps1 +10 -3
  209. package/dist/keymap.d.ts +41 -0
  210. package/dist/keymap.js +51 -0
  211. package/dist/knowledge-cli.js +1 -1
  212. package/dist/knowledge-ui.d.ts +21 -4
  213. package/dist/knowledge-ui.js +75 -7
  214. package/dist/laptop-background.d.ts +16 -0
  215. package/dist/laptop-background.js +108 -2
  216. package/dist/lead-cli.js +26 -24
  217. package/dist/lead-context.js +1 -1
  218. package/dist/lead-contract.d.ts +1 -1
  219. package/dist/lead-contract.js +3 -2
  220. package/dist/lead-doors.d.ts +4 -0
  221. package/dist/lead-doors.js +2 -2
  222. package/dist/lead-progress.js +5 -0
  223. package/dist/lead-status.d.ts +2 -0
  224. package/dist/lead-status.js +6 -2
  225. package/dist/lead-tools.js +162 -34
  226. package/dist/lead.js +2 -2
  227. package/dist/lesson-notes.d.ts +81 -0
  228. package/dist/lesson-notes.js +336 -0
  229. package/dist/limits-ui.js +1 -1
  230. package/dist/local-owner.d.ts +1 -1
  231. package/dist/local-owner.js +16 -0
  232. package/dist/machine-check.d.ts +16 -0
  233. package/dist/machine-check.js +26 -0
  234. package/dist/machine-identity.d.ts +75 -0
  235. package/dist/machine-identity.js +170 -0
  236. package/dist/mcp-core.js +11 -1
  237. package/dist/memory-cli.d.ts +300 -1
  238. package/dist/memory-cli.js +62 -9
  239. package/dist/memory-pass.d.ts +16 -0
  240. package/dist/memory-pass.js +67 -3
  241. package/dist/memory-tidy.d.ts +181 -0
  242. package/dist/memory-tidy.js +523 -0
  243. package/dist/memory-use.d.ts +35 -0
  244. package/dist/memory-use.js +109 -0
  245. package/dist/merge-checklist.d.ts +55 -0
  246. package/dist/merge-checklist.js +182 -0
  247. package/dist/modes.d.ts +6 -6
  248. package/dist/modes.js +3 -1
  249. package/dist/needs-you.d.ts +31 -9
  250. package/dist/needs-you.js +110 -41
  251. package/dist/nightly-github.d.ts +2 -0
  252. package/dist/nightly-github.js +4 -4
  253. package/dist/operate-remote-arguments.js +7 -1
  254. package/dist/operate-remote.js +3 -1
  255. package/dist/operate.d.ts +6 -1
  256. package/dist/operate.js +565 -64
  257. package/dist/phase-routing.js +2 -2
  258. package/dist/plain-schema.d.ts +23 -0
  259. package/dist/plain-schema.js +94 -0
  260. package/dist/plain-words.d.ts +20 -0
  261. package/dist/plain-words.js +66 -0
  262. package/dist/plan-login.d.ts +14 -0
  263. package/dist/plan-login.js +58 -0
  264. package/dist/planner.d.ts +1 -1
  265. package/dist/planner.js +4 -2
  266. package/dist/process-liveness.d.ts +2 -0
  267. package/dist/process-liveness.js +61 -17
  268. package/dist/project-delete-ui.d.ts +19 -2
  269. package/dist/project-delete-ui.js +29 -1
  270. package/dist/project-delete.js +14 -1
  271. package/dist/project-knowledge.d.ts +18 -11
  272. package/dist/project-knowledge.js +39 -9
  273. package/dist/project-learning.d.ts +2 -1
  274. package/dist/project-learning.js +15 -45
  275. package/dist/project-memory.d.ts +5 -0
  276. package/dist/project-memory.js +22 -2
  277. package/dist/project-tools.d.ts +4 -4
  278. package/dist/project-tools.js +23 -7
  279. package/dist/proof.js +30 -5
  280. package/dist/protected-storage-look.d.ts +1 -0
  281. package/dist/protected-storage-look.js +13 -0
  282. package/dist/protected-storage.d.ts +70 -39
  283. package/dist/protected-storage.js +196 -147
  284. package/dist/provider-auth.js +1 -1
  285. package/dist/provider-capture.d.ts +34 -0
  286. package/dist/provider-capture.js +53 -0
  287. package/dist/publish.js +9 -1
  288. package/dist/pull-request-flow.js +5 -0
  289. package/dist/push.d.ts +18 -2
  290. package/dist/push.js +40 -16
  291. package/dist/release-adapters.d.ts +9 -0
  292. package/dist/release-adapters.js +76 -0
  293. package/dist/release-auto.js +13 -13
  294. package/dist/release-card.d.ts +1 -2
  295. package/dist/release-card.js +11 -0
  296. package/dist/release-carry.d.ts +7 -2
  297. package/dist/release-carry.js +49 -10
  298. package/dist/release-cli.d.ts +4 -0
  299. package/dist/release-cli.js +29 -22
  300. package/dist/release-coverage.js +1 -1
  301. package/dist/release-next.d.ts +48 -0
  302. package/dist/release-next.js +161 -0
  303. package/dist/release.d.ts +20 -1
  304. package/dist/release.js +85 -9
  305. package/dist/remote-destination.d.ts +36 -0
  306. package/dist/remote-destination.js +217 -0
  307. package/dist/remote-main.d.ts +48 -0
  308. package/dist/remote-main.js +84 -5
  309. package/dist/repository-context.d.ts +3 -2
  310. package/dist/repository-context.js +112 -70
  311. package/dist/restart-certification.d.ts +1 -0
  312. package/dist/restart-certification.js +8 -6
  313. package/dist/result-acts.d.ts +27 -19
  314. package/dist/result-acts.js +36 -25
  315. package/dist/result-follow-ups.d.ts +2 -2
  316. package/dist/result-follow-ups.js +18 -4
  317. package/dist/result-review.d.ts +1 -1
  318. package/dist/route-change-check.d.ts +4 -0
  319. package/dist/route-change-check.js +13 -0
  320. package/dist/route-change.d.ts +22 -0
  321. package/dist/route-change.js +89 -0
  322. package/dist/route-hooks.js +3 -2
  323. package/dist/route-results.d.ts +6 -1
  324. package/dist/route-results.js +14 -5
  325. package/dist/run-closeout.d.ts +1 -1
  326. package/dist/run-closeout.js +7 -5
  327. package/dist/runner-process.js +3 -2
  328. package/dist/runner.js +2 -1
  329. package/dist/scope.d.ts +28 -4
  330. package/dist/scope.js +92 -28
  331. package/dist/scout.d.ts +1 -1
  332. package/dist/scout.js +2 -1
  333. package/dist/serve.d.ts +1 -1
  334. package/dist/serve.js +26 -18
  335. package/dist/server/chat.js +16 -14
  336. package/dist/server/chrome.d.ts +1 -1
  337. package/dist/server/chrome.js +26 -35
  338. package/dist/server/console.css +73 -0
  339. package/dist/server/guards.js +3 -0
  340. package/dist/server/home.d.ts +6 -0
  341. package/dist/server/home.js +21 -13
  342. package/dist/server/http.js +1 -1
  343. package/dist/server/kit.js +1 -1
  344. package/dist/server/merge-ready-view.d.ts +10 -0
  345. package/dist/server/merge-ready-view.js +30 -0
  346. package/dist/server/pages-assets.d.ts +1 -0
  347. package/dist/server/pages-assets.js +8 -5
  348. package/dist/server/pages.js +103 -82
  349. package/dist/server/render-chat.js +16 -3
  350. package/dist/server/render-code-map.d.ts +30 -0
  351. package/dist/server/render-code-map.js +137 -0
  352. package/dist/server/render-flaky.d.ts +20 -0
  353. package/dist/server/render-flaky.js +59 -0
  354. package/dist/server/render-follow-through.d.ts +11 -0
  355. package/dist/server/render-follow-through.js +68 -0
  356. package/dist/server/render-history.js +1 -1
  357. package/dist/server/render-new-task.d.ts +26 -1
  358. package/dist/server/render-new-task.js +30 -5
  359. package/dist/server/render-pages.d.ts +1 -175
  360. package/dist/server/render-pages.js +38 -313
  361. package/dist/server/render-projects.d.ts +2 -1
  362. package/dist/server/render-projects.js +9 -7
  363. package/dist/server/render-release.d.ts +2 -0
  364. package/dist/server/render-release.js +3 -3
  365. package/dist/server/render-results.d.ts +2 -0
  366. package/dist/server/render-results.js +68 -68
  367. package/dist/server/render-tasks.d.ts +16 -2
  368. package/dist/server/render-tasks.js +177 -141
  369. package/dist/server/retired-routes.d.ts +17 -0
  370. package/dist/server/retired-routes.js +53 -0
  371. package/dist/server/route-table.d.ts +3 -0
  372. package/dist/server/route-table.js +48 -12
  373. package/dist/server/settings.js +166 -34
  374. package/dist/server/task-views.js +66 -26
  375. package/dist/server/tasks.d.ts +2 -6
  376. package/dist/server/tasks.js +250 -321
  377. package/dist/server/welcome.js +1 -1
  378. package/dist/settings-groups-ui.d.ts +1 -0
  379. package/dist/settings-groups-ui.js +4 -2
  380. package/dist/settings-kit.js +3 -2
  381. package/dist/settings-proofs.js +3 -1
  382. package/dist/slack-chat.js +14 -1
  383. package/dist/steer-mailbox.d.ts +8 -1
  384. package/dist/steer-mailbox.js +164 -6
  385. package/dist/stop-proof.js +6 -4
  386. package/dist/storage-sweep-process.js +1 -1
  387. package/dist/storage-sweep.d.ts +5 -3
  388. package/dist/storage-sweep.js +30 -9
  389. package/dist/storage-ui.js +28 -17
  390. package/dist/storage.d.ts +27 -2
  391. package/dist/storage.js +39 -2
  392. package/dist/store.d.ts +89 -7
  393. package/dist/store.js +386 -114
  394. package/dist/surface.js +67 -6
  395. package/dist/sweep-quarantine.d.ts +1 -0
  396. package/dist/sweep-quarantine.js +6 -4
  397. package/dist/tamper-scan.d.ts +4 -0
  398. package/dist/tamper-scan.js +90 -0
  399. package/dist/task-aliases.d.ts +5 -0
  400. package/dist/task-aliases.js +9 -0
  401. package/dist/task-control.d.ts +2 -1
  402. package/dist/task-control.js +4 -1
  403. package/dist/task-outcome-cli.d.ts +2 -0
  404. package/dist/task-outcome-cli.js +17 -17
  405. package/dist/task-sizing.d.ts +5 -0
  406. package/dist/task-sizing.js +2 -0
  407. package/dist/task-source.d.ts +7 -0
  408. package/dist/task-source.js +14 -0
  409. package/dist/task-status.d.ts +9 -0
  410. package/dist/task-status.js +45 -49
  411. package/dist/teardown-git.d.ts +18 -0
  412. package/dist/teardown-git.js +105 -21
  413. package/dist/telegram-lead.js +23 -6
  414. package/dist/telegram-progress.js +5 -3
  415. package/dist/telegram-status.js +6 -4
  416. package/dist/telegram.js +12 -1
  417. package/dist/test-infra.d.ts +8 -0
  418. package/dist/test-infra.js +18 -0
  419. package/dist/tidy.js +1 -1
  420. package/dist/toolroll-update.js +19 -5
  421. package/dist/tui-contract.d.ts +24 -0
  422. package/dist/tui-source.js +67 -8
  423. package/dist/tui.js +51 -51
  424. package/dist/vanished-retry.d.ts +30 -0
  425. package/dist/vanished-retry.js +174 -0
  426. package/dist/work-index.d.ts +2 -0
  427. package/dist/work-index.js +95 -66
  428. package/dist/work-summary.d.ts +1 -1
  429. package/dist/work-summary.js +7 -2
  430. package/dist/workspace-ui.d.ts +7 -3
  431. package/dist/workspace-ui.js +38 -23
  432. package/dist/worktree.d.ts +12 -3
  433. package/dist/worktree.js +91 -14
  434. package/package.json +2 -1
  435. package/dist/server/render-next.d.ts +0 -63
  436. package/dist/server/render-next.js +0 -109
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The storage check's child process (protected-storage.ts prepareProtectedStorage): it finds the protected roots, reads
3
+ * Firefox's profiles.ini and walks every present root here, where a listing that waits on a macOS privacy prompt holds
4
+ * up only this process. It writes `at\t<root>` before each walk and `done\t<answer>` at the end; the parent kills it at
5
+ * its limit and refuses the launch.
6
+ */
7
+ import { protectedStorageHere } from "./protected-storage.js";
8
+ const request = JSON.parse(process.argv[2] ?? "{}");
9
+ if (typeof request.home !== "string" || typeof request.platform !== "string" || request.env === null || typeof request.env !== "object")
10
+ process.exit(2);
11
+ const env = Object.fromEntries(Object.entries(request.env).filter((one) => typeof one[1] === "string"));
12
+ const answer = protectedStorageHere(request.home, env, request.platform, () => { }, undefined, root => { process.stdout.write(`at\t${JSON.stringify(root)}\n`); });
13
+ process.stdout.write(`done\t${JSON.stringify(answer)}\n`);
@@ -21,47 +21,78 @@ export declare const PROTECTED_WALK: {
21
21
  };
22
22
  /** The roots by name, under the (real) home folder and the overrides the plane's environment names. */
23
23
  export declare function protectedStorageNames(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform): string[];
24
+ /** What one launch keeps out: every root by name (`names`), and every protected path it denies (`denies`). */
25
+ export type ProtectedStorage = {
26
+ names: string[];
27
+ denies: string[];
28
+ };
24
29
  /**
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).
30
+ * The storage check, run right here: every root by name and by real path, every link inside a present root that leads
31
+ * outward (where it leads, by real path and as spelled), and the overrides. `unfinished` names each present root whose
32
+ * walk met an error other than "not there" or "not allowed": its links aren't all known. A link macOS makes in a
33
+ * sandboxed app's container to the same place under the home folder (its Desktop, Downloads, Fonts…) is the system's
34
+ * own mirror of the person's folders, not the app's data, and isn't followed (systemMirror).
29
35
  *
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.
36
+ * On a Mac a listing (or an open) inside another app's data can wait until a person answers a privacy prompt, so there
37
+ * this runs only in the check's own child process (prepareProtectedStorage), never on a thread that serves anything.
38
+ * `at` is told each root before its walk starts, so a check that runs out of time can name the app it was waiting on.
37
39
  */
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";
40
+ export declare function protectedStorageHere(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, say?: (line: string) => void, list?: (path: string) => import("node:fs").Dirent[], at?: (root: string) => void): ProtectedStorage & {
41
+ unfinished: string[];
42
+ };
43
+ /** Every protected path, checked right here (protectedStorageHere), for a caller that has no prepared check. */
44
+ export declare function protectedStorageDenies(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, say?: (line: string) => void, list?: (path: string) => import("node:fs").Dirent[]): string[];
45
+ /** How long the storage check may take before the launch is refused (a slow Mac finishes well inside it). */
46
+ export declare const STORAGE_LOOK_MS = 10000;
47
+ /** What the check asked of its child, and how the child's run ended. */
48
+ export type StorageLookRequest = {
49
+ home: string;
50
+ env: Record<string, string>;
51
+ platform: NodeJS.Platform;
52
+ };
53
+ export type StorageLookResult = {
54
+ kind: "done";
55
+ storage: ProtectedStorage;
56
+ unfinished: string[];
57
+ } | {
58
+ kind: "timed-out";
59
+ at: string | null;
60
+ } | {
61
+ kind: "failed";
62
+ detail: string;
63
+ };
64
+ export type StorageLook = (request: StorageLookRequest) => Promise<StorageLookResult>;
65
+ /** Why a launch didn't start: the storage check couldn't finish. Its words survive every catch (storageRefusalOf). */
66
+ export declare class ProtectedStorageRefusal extends Error {
67
+ readonly apps: readonly string[];
68
+ readonly why: "timed-out" | "unfinished" | "failed";
69
+ readonly code: "protected-storage";
70
+ constructor(apps: readonly string[], why: "timed-out" | "unfinished" | "failed", message: string);
71
+ }
72
+ /** The storage refusal inside a caught error, when it is one. */
73
+ export declare function storageRefusalOf(error: unknown): ProtectedStorageRefusal | null;
74
+ /** Which program macOS must allow: the one this plane runs as. A launchd service is its program itself; a plane started
75
+ * from a terminal asks as that terminal app. */
76
+ export declare function storageAccessHolder(env?: Readonly<Record<string, string | undefined>>, execPath?: string, versions?: NodeJS.ProcessVersions): string;
77
+ /** Why nothing was launched, and what the person does about it. Never says macOS is asking: that isn't known. */
78
+ export declare function storageRefusalWords(apps: readonly string[], why: ProtectedStorageRefusal["why"], holder: string): string;
44
79
  /**
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.
80
+ * The storage check for one launch, awaited: what it denies, from a walk that finished just now. On a Mac the whole check
81
+ * (finding the roots, reading profiles.ini, every walk) runs in a child process, so a listing that waits on a privacy
82
+ * prompt holds up only that child; the thread that asked keeps serving. A check that doesn't finish within
83
+ * STORAGE_LOOK_MS, fails, or leaves a present root's walk unfinished refuses the launch (ProtectedStorageRefusal): no
84
+ * earlier walk stands in for it, since a link can move inside a root at any time. Checks asked for at once share one child.
50
85
  */
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;
86
+ export declare function prepareProtectedStorage(home?: string, env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, look?: StorageLook, holder?: string): Promise<ProtectedStorage>;
87
+ /** Where the check's child script is: beside this module once built, else the built copy (a run from source). */
88
+ export declare function storageLookScript(): string;
89
+ /** A finished child's lines as the check's answer: `at\t<root>` before each walk, `done\t<answer>` once at the end. */
90
+ export declare function readLook(ran: SpawnedLook): StorageLookResult;
91
+ /** How a child ran: everything it wrote, and whether it finished, ran out of time (killed) or failed some other way. */
92
+ export type SpawnedLook = {
93
+ stdout: string;
94
+ outcome: "done" | "timed-out" | "failed";
95
+ detail: string;
96
+ };
97
+ /** Run a child without waiting on this thread: its output, bounded, and how it ended. Killed at `limitMs`. */
98
+ export declare function spawnLook(command: string, args: readonly string[], limitMs: number): Promise<SpawnedLook>;
@@ -15,14 +15,18 @@
15
15
  * OP_CONFIG_DIR, BITWARDENCLI_APPDATA_DIR, and the profiles Firefox's (and Zen's) profiles.ini places elsewhere.
16
16
  * Storage Toolroll doesn't know about (a browser started with its own --user-data-dir, a portable app) isn't covered.
17
17
  *
18
+ * Each launch's check is its own and finishes before the launch (prepareProtectedStorage): on a Mac it runs in a child
19
+ * process, so a privacy prompt never holds the thread that serves the console, and a check that can't finish refuses
20
+ * the launch in words that say which program macOS may need to allow. No earlier walk stands in for it.
21
+ *
18
22
  * The chat and vault variables (SLACK_*, TELEGRAM_*, DISCORD_*, TEAMS_*, OP_*, BW_*, BITWARDENCLI_*) are kept out of
19
23
  * every launch's environment (`withoutProtectedEnv`, exec.ts).
20
24
  */
21
- import { spawnSync } from "node:child_process";
22
- import { closeSync, existsSync, fstatSync, lstatSync, openSync, constants as fsConstants, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
25
+ import { spawn } from "node:child_process";
26
+ import { closeSync, existsSync, fstatSync, lstatSync, openSync, constants as fsConstants, readSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
23
27
  import { homedir } from "node:os";
24
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
25
- import { configBase, namedFolder } from "./names.js";
28
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
29
+ import { fileURLToPath } from "node:url";
26
30
  const AS = "Library/Application Support";
27
31
  const CONTAINERS = "Library/Containers";
28
32
  const GROUPS = "Library/Group Containers";
@@ -127,28 +131,38 @@ export function protectedStorageNames(home = homedir(), env = process.env, platf
127
131
  add(profile);
128
132
  return [...names].sort();
129
133
  }
130
- /** The absolute `Path=` entries of a profiles.ini in `root`, read only as the plain file it is, bounded. */
134
+ /** The absolute `Path=` entries of a profiles.ini in `root`, read only as the plain file it is, bounded. On a Mac this
135
+ * runs only inside the storage check's child process (prepareProtectedStorage), where an open that waits on macOS
136
+ * holds up nothing else. */
131
137
  function profilesElsewhere(root) {
132
- const file = join(root, "profiles.ini");
138
+ const text = profilesIniHere(join(root, "profiles.ini"));
139
+ return (text ?? "").split(/\r?\n/)
140
+ .map(line => /^\s*Path\s*=\s*(.+?)\s*$/i.exec(line)?.[1] ?? null)
141
+ .filter((path) => path !== null && isAbsolute(path));
142
+ }
143
+ function profilesIniHere(file) {
144
+ try {
145
+ lstatSync(file);
146
+ }
147
+ catch {
148
+ return null;
149
+ }
133
150
  let fd;
134
151
  try {
135
152
  fd = openSync(file, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
136
153
  }
137
154
  catch {
138
- return [];
155
+ return null;
139
156
  }
140
157
  try {
141
158
  const held = fstatSync(fd);
142
159
  if (!held.isFile())
143
- return [];
160
+ return null;
144
161
  const buffer = Buffer.alloc(Math.min(held.size, 64 * 1024));
145
- const read = readSync(fd, buffer, 0, buffer.length, 0);
146
- return buffer.subarray(0, read).toString("utf8").split(/\r?\n/)
147
- .map(line => /^\s*Path\s*=\s*(.+?)\s*$/i.exec(line)?.[1] ?? null)
148
- .filter((path) => path !== null && isAbsolute(path));
162
+ return buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
149
163
  }
150
164
  catch {
151
- return [];
165
+ return null;
152
166
  }
153
167
  finally {
154
168
  closeSync(fd);
@@ -156,26 +170,24 @@ function profilesElsewhere(root) {
156
170
  }
157
171
  const unwalkedSaid = new Set();
158
172
  /**
159
- * Every protected path one launch denies: each root by name and by real path, every link inside a present root that
160
- * leads outward (where it leads, by real path and as spelled), and the overrides. Resolved at each launch, never kept.
161
- * A link macOS makes in a sandboxed app's container to the same place under the home folder (its Desktop, Downloads,
162
- * Fonts…) is the system's own mirror of the person's folders, not the app's data, and isn't followed (systemMirror).
173
+ * The storage check, run right here: every root by name and by real path, every link inside a present root that leads
174
+ * outward (where it leads, by real path and as spelled), and the overrides. `unfinished` names each present root whose
175
+ * walk met an error other than "not there" or "not allowed": its links aren't all known. A link macOS makes in a
176
+ * sandboxed app's container to the same place under the home folder (its Desktop, Downloads, Fonts…) is the system's
177
+ * own mirror of the person's folders, not the app's data, and isn't followed (systemMirror).
163
178
  *
164
- * On a Mac, listing inside another app's container waits for the person to answer macOS's "access data from other apps"
165
- * prompt when the process asking has no answer yet (a plane run by launchd: every launch, and the console with it, hung
166
- * there in release check 3249). So the containers are walked only after a time-limited look in a child process
167
- * (`containers`, containersListable) shows they can be listed without waiting. A look that fails any other way refuses
168
- * the launch (it throws). When macOS is waiting on the person, the containers stay denied by name and real path, and
169
- * the outward links the last complete walk found (kept in the plane's own state folder, linkCacheFile) stay denied too;
170
- * with none kept, only the roots are denied, and that is said once.
179
+ * On a Mac a listing (or an open) inside another app's data can wait until a person answers a privacy prompt, so there
180
+ * this runs only in the check's own child process (prepareProtectedStorage), never on a thread that serves anything.
181
+ * `at` is told each root before its walk starts, so a check that runs out of time can name the app it was waiting on.
171
182
  */
172
- export function protectedStorageDenies(home = homedir(), env = process.env, platform = process.platform, say = line => process.stderr.write(`${line}\n`), containers = roots => containersListable(roots), list = path => readdirSync(path, { withFileTypes: true })) {
183
+ export function protectedStorageHere(home = homedir(), env = process.env, platform = process.platform, say = () => { }, list = path => readdirSync(path, { withFileTypes: true }), at = () => { }) {
173
184
  const base = real(home);
185
+ const names = protectedStorageNames(base, env, platform);
174
186
  const out = new Set();
175
187
  const add = (path) => { if (!tooWide(path, base))
176
188
  out.add(path); };
177
- const present = [];
178
- for (const name of protectedStorageNames(base, env, platform)) {
189
+ const unfinished = [];
190
+ for (const name of names) {
179
191
  add(name);
180
192
  try {
181
193
  lstatSync(name);
@@ -185,36 +197,18 @@ export function protectedStorageDenies(home = homedir(), env = process.env, plat
185
197
  }
186
198
  const root = real(name);
187
199
  add(root);
188
- present.push({ name, root });
189
- }
190
- const held = platform === "darwin" ? present.filter(one => inAppContainer(one.root, base)).map(one => one.root) : [];
191
- // Throws when the look itself failed: the launch is refused, never fenced with less.
192
- const listable = held.length === 0 || containers(held);
193
- const cache = held.length === 0 ? null : linkCacheFile(base, env);
194
- if (!listable) {
195
- const kept = cache === null ? null : readLinkCache(cache, base);
196
- for (const target of kept ?? [])
200
+ at(root);
201
+ const walked = { complete: true };
202
+ for (const target of outwardLinks(name, root, base, say, list, walked))
197
203
  add(target);
198
- if (!unwalkedSaid.has(CONTAINERS_UNLISTED)) {
199
- unwalkedSaid.add(CONTAINERS_UNLISTED);
200
- say(`toolroll: fence: macOS didn't let Toolroll look inside other apps' containers without asking you; they stay denied, ${kept === null ? "but links inside them weren't checked" : "with the links found inside them when they were last checked"}. Allow Toolroll's service to access data from other apps (Privacy & Security) to check them now.`);
201
- }
204
+ if (!walked.complete && !unfinished.includes(root))
205
+ unfinished.push(root);
202
206
  }
203
- const walked = { complete: true }, inside = [];
204
- for (const { name, root } of present) {
205
- const container = held.includes(root);
206
- if (container && !listable)
207
- continue;
208
- for (const target of outwardLinks(name, root, base, say, list, container ? walked : undefined)) {
209
- add(target);
210
- if (container)
211
- inside.push(target);
212
- }
213
- }
214
- // Only a walk of every container that met no error but "not there" or "not allowed" is kept for later.
215
- if (listable && held.length > 0 && walked.complete && cache !== null)
216
- writeLinkCache(cache, base, inside);
217
- return [...out].sort();
207
+ return { names, denies: [...out].sort(), unfinished };
208
+ }
209
+ /** Every protected path, checked right here (protectedStorageHere), for a caller that has no prepared check. */
210
+ export function protectedStorageDenies(home = homedir(), env = process.env, platform = process.platform, say, list) {
211
+ return protectedStorageHere(home, env, platform, say, list).denies;
218
212
  }
219
213
  /** Where the links inside a present root lead, when outside it: a walk that never follows a link, bounded. */
220
214
  function outwardLinks(name, root, home, say, list, walked) {
@@ -289,110 +283,165 @@ function systemMirror(link, root, target, home) {
289
283
  const known = CONTAINER_MIRRORS.has(named) || named === `Library/Application Scripts/${root.split(sep).at(-1)}`;
290
284
  return known && target === join(home, ...under);
291
285
  }
292
- /** The key `unwalkedSaid` keeps for "the containers couldn't be listed" (said once per process). */
293
- const CONTAINERS_UNLISTED = "\0containers";
294
- /** A root directly in the home folder's Library/Containers or Library/Group Containers: another app's container. */
295
- function inAppContainer(root, home) {
296
- return dirname(root) === real(join(home, CONTAINERS)) || dirname(root) === real(join(home, GROUPS));
286
+ /** The answers a walk takes as complete: the folder isn't there, or macOS refused it outright. */
287
+ const ANSWERED = new Set(["ENOENT", "ENOTDIR", "EPERM", "EACCES"]);
288
+ // ---- the check, off the thread ---------------------------------------------------------------------------------
289
+ /** How long the storage check may take before the launch is refused (a slow Mac finishes well inside it). */
290
+ export const STORAGE_LOOK_MS = 10_000;
291
+ /** At most this much of the check's answer is read. */
292
+ const LOOK_OUTPUT_MAX = 8 * 1024 * 1024;
293
+ /** The plane's own variables the check reads (protectedStorageNames), handed to its child as they are here. */
294
+ const STORAGE_ENV = ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "OP_CONFIG_DIR", "BITWARDENCLI_APPDATA_DIR"];
295
+ /** Why a launch didn't start: the storage check couldn't finish. Its words survive every catch (storageRefusalOf). */
296
+ export class ProtectedStorageRefusal extends Error {
297
+ apps;
298
+ why;
299
+ code = "protected-storage";
300
+ constructor(apps, why, message) {
301
+ super(message);
302
+ this.apps = apps;
303
+ this.why = why;
304
+ this.name = "ProtectedStorageRefusal";
305
+ }
297
306
  }
298
- /** How long the look at other apps' containers may take before Toolroll takes it that macOS is waiting on the person. */
299
- export const CONTAINER_LOOK_MS = 3_000;
300
- /** How long a "can't list them" answer stands before the next launch looks again (the person may have allowed it since). */
301
- const CONTAINER_RELOOK_MS = 10 * 60_000;
302
- /** Each set of container roots' last answer: listable, or not and when that was seen. */
303
- const containerLooks = new Map();
304
- /** The look itself, in a child process: the same bounded walk `outwardLinks` makes, never following a link. */
305
- const CONTAINER_LOOK = `const fs=require("fs"),path=require("path");const [depth,limit,...roots]=process.argv.slice(1);
306
- for(const root of roots){let seen=0;const queue=[{p:root,d:0}];while(queue.length>0){const {p,d}=queue.shift();let entries;
307
- try{entries=fs.readdirSync(p,{withFileTypes:true});}catch{continue;}for(const e of entries){if(++seen>Number(limit)){queue.length=0;break;}
308
- if(!e.isSymbolicLink()&&e.isDirectory()&&d+1<Number(depth))queue.push({p:path.join(p,e.name),d:d+1});}}}`;
309
- /**
310
- * Whether this process can list inside these app containers without macOS stopping to ask the person: the walk run
311
- * first in a child process that is killed at CONTAINER_LOOK_MS. Finishing in time (listed, or refused outright) means
312
- * the walk can run here too; only running into that limit means macOS is waiting on the person (only the child waited).
313
- * A look that fails any other way is tried once more, then throws: the launch is refused rather than fenced with less.
314
- * A yes is kept (macOS keeps the person's answer); a no is looked at again after CONTAINER_RELOOK_MS.
315
- */
316
- export function containersListable(roots, look = lookInContainers, now = Date.now) {
317
- const key = [...roots].sort().join("\0");
318
- const last = containerLooks.get(key);
319
- if (last !== undefined && (last.listable || now() - last.at < CONTAINER_RELOOK_MS))
320
- return last.listable;
321
- let answer = look(roots);
322
- if (answer === "failed")
323
- answer = look(roots);
324
- if (answer === "failed")
325
- throw new Error("Toolroll couldn't look inside other apps' containers to fence them (the look failed twice), so nothing was launched.");
326
- const listable = answer === "listable";
327
- containerLooks.set(key, { listable, at: now() });
328
- return listable;
307
+ /** The storage refusal inside a caught error, when it is one. */
308
+ export function storageRefusalOf(error) {
309
+ return error instanceof ProtectedStorageRefusal ? error : null;
329
310
  }
330
- /** The child-process look (containersListable): node (or Electron as node) walks the roots, killed at its limit. */
331
- function lookInContainers(roots) {
332
- const result = spawnSync(process.execPath, ["-e", CONTAINER_LOOK, String(PROTECTED_WALK.depth), String(PROTECTED_WALK.entries), ...roots], { stdio: "ignore", timeout: CONTAINER_LOOK_MS, killSignal: "SIGKILL", env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" } });
333
- return lookOutcome(result);
311
+ /** The apps behind these roots, by name, for the words a person reads. */
312
+ function appsOf(roots, home) {
313
+ const names = roots.map(root => PROTECTED_STORAGE.find(row => [...row.darwin, ...row.linux].some(one => real(join(home, one)) === root || join(home, one) === root))?.app ?? basename(root));
314
+ return [...new Set(names)];
334
315
  }
335
- /** A look's outcome from its child's result: only its own limit (ETIMEDOUT) is macOS waiting; any other error, signal
336
- * or exit code is a failed look. */
337
- export function lookOutcome(result) {
338
- if (result.error?.code === "ETIMEDOUT")
339
- return "waiting";
340
- return result.error === undefined && result.signal === null && result.status === 0 ? "listable" : "failed";
316
+ /** Which program macOS must allow: the one this plane runs as. A launchd service is its program itself; a plane started
317
+ * from a terminal asks as that terminal app. */
318
+ export function storageAccessHolder(env = process.env, execPath = process.execPath, versions = process.versions) {
319
+ if (versions["electron"] !== undefined)
320
+ return "the Toolroll app";
321
+ const service = env["XPC_SERVICE_NAME"];
322
+ if (service !== undefined && service !== "" && service !== "0")
323
+ return `the program that keeps Toolroll running (${execPath})`;
324
+ const terminal = env["TERM_PROGRAM"];
325
+ if (terminal !== undefined && terminal !== "")
326
+ return `the terminal app Toolroll was started from (${terminal.replace(/\.app$/, "")})`;
327
+ return `the program Toolroll runs as (${execPath})`;
341
328
  }
342
- /** The answers a container walk takes as complete: the folder isn't there, or macOS refused it outright. */
343
- const ANSWERED = new Set(["ENOENT", "ENOTDIR", "EPERM", "EACCES"]);
344
- /** At most this much is ever read back from the link cache. */
345
- const LINK_CACHE_MAX = 256 * 1024;
346
- /** Where the outward links the last complete container walk found are kept: the plane's own state folder (fenced from
347
- * every build), one file per user. */
348
- export function linkCacheFile(home, env = process.env) {
349
- return join(namedFolder(configBase({ ...env }, home)), "protected-links.json");
329
+ /** Why nothing was launched, and what the person does about it. Never says macOS is asking: that isn't known. */
330
+ export function storageRefusalWords(apps, why, holder) {
331
+ const named = apps.length === 0 ? "other apps" : apps.length === 1 ? apps[0] : `${apps.slice(0, -1).join(", ")} and ${apps.at(-1)}`;
332
+ const reason = why === "timed-out" ? ` (the check didn't finish within ${STORAGE_LOOK_MS / 1000} seconds)` : why === "unfinished" ? " (some of its folders couldn't be read)" : " (the check stopped with an error)";
333
+ return `Nothing was started: Toolroll couldn't finish checking ${apps.length === 0 ? "other apps'" : `${named}'s`} data for links to keep out of the build${reason}. `
334
+ + `macOS may need to let ${holder} read it: in System Settings → Privacy & Security → Full Disk Access, turn it on for ${holder}, then try again.`;
350
335
  }
351
- /** The kept links for this home: only a plain file of this user's (no link), at most LINK_CACHE_MAX, of exactly the
352
- * kept shape; anything else is no cache at all. */
353
- export function readLinkCache(file, home) {
354
- let fd;
355
- try {
356
- const held = lstatSync(file);
357
- if (!held.isFile() || held.size > LINK_CACHE_MAX || (process.getuid !== undefined && held.uid !== process.getuid()))
358
- return null;
359
- fd = openSync(file, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
360
- }
361
- catch {
362
- return null;
336
+ const inFlight = new Map();
337
+ /**
338
+ * The storage check for one launch, awaited: what it denies, from a walk that finished just now. On a Mac the whole check
339
+ * (finding the roots, reading profiles.ini, every walk) runs in a child process, so a listing that waits on a privacy
340
+ * prompt holds up only that child; the thread that asked keeps serving. A check that doesn't finish within
341
+ * STORAGE_LOOK_MS, fails, or leaves a present root's walk unfinished refuses the launch (ProtectedStorageRefusal): no
342
+ * earlier walk stands in for it, since a link can move inside a root at any time. Checks asked for at once share one child.
343
+ */
344
+ export async function prepareProtectedStorage(home = homedir(), env = process.env, platform = process.platform, look = lookInChild, holder = storageAccessHolder(env)) {
345
+ const base = real(home);
346
+ const request = { home: base, env: Object.fromEntries(STORAGE_ENV.flatMap(name => env[name] === undefined ? [] : [[name, env[name]]])), platform };
347
+ let answer;
348
+ if (platform !== "darwin") {
349
+ // No privacy prompt can hold a listing here: the check runs as it is.
350
+ const here = protectedStorageHere(base, env, platform, line => process.stderr.write(`${line}\n`));
351
+ answer = { kind: "done", storage: { names: here.names, denies: here.denies }, unfinished: here.unfinished };
363
352
  }
364
- try {
365
- const opened = fstatSync(fd);
366
- if (!opened.isFile() || opened.size > LINK_CACHE_MAX || (process.getuid !== undefined && opened.uid !== process.getuid()))
367
- return null;
368
- const buffer = Buffer.alloc(opened.size);
369
- const read = readSync(fd, buffer, 0, buffer.length, 0);
370
- const value = JSON.parse(buffer.subarray(0, read).toString("utf8"));
371
- if (value === null || typeof value !== "object" || Array.isArray(value))
372
- return null;
373
- const { version, home: kept, targets } = value;
374
- if (version !== 1 || kept !== home || !Array.isArray(targets) || targets.some(one => typeof one !== "string" || !isAbsolute(one)))
375
- return null;
376
- return targets;
353
+ else {
354
+ const key = JSON.stringify(request);
355
+ let pending = inFlight.get(key);
356
+ if (pending === undefined) {
357
+ pending = look(request).finally(() => inFlight.delete(key));
358
+ inFlight.set(key, pending);
359
+ }
360
+ answer = await pending;
377
361
  }
378
- catch {
379
- return null;
362
+ if (answer.kind === "timed-out") {
363
+ const apps = answer.at === null ? [] : appsOf([answer.at], base);
364
+ throw new ProtectedStorageRefusal(apps, "timed-out", storageRefusalWords(apps, "timed-out", holder));
380
365
  }
381
- finally {
382
- closeSync(fd);
366
+ if (answer.kind === "failed")
367
+ throw new ProtectedStorageRefusal([], "failed", storageRefusalWords([], "failed", holder));
368
+ if (answer.unfinished.length > 0) {
369
+ const apps = appsOf(answer.unfinished, base);
370
+ throw new ProtectedStorageRefusal(apps, "unfinished", storageRefusalWords(apps, "unfinished", holder));
383
371
  }
372
+ return answer.storage;
373
+ }
374
+ /** Where the check's child script is: beside this module once built, else the built copy (a run from source). */
375
+ export function storageLookScript() {
376
+ const beside = fileURLToPath(new URL("./protected-storage-look.js", import.meta.url));
377
+ return existsSync(beside) ? beside : fileURLToPath(new URL("../dist/protected-storage-look.js", import.meta.url));
378
+ }
379
+ /** The default check: node (or Electron as node) runs protected-storage-look.js, killed at STORAGE_LOOK_MS. */
380
+ async function lookInChild(request) {
381
+ return readLook(await spawnLook(process.execPath, [storageLookScript(), JSON.stringify(request)], STORAGE_LOOK_MS));
384
382
  }
385
- /** Keep a complete walk's links: written aside (owner-only) and renamed into place, only into a state folder that is
386
- * already there. A failed write keeps whatever was kept before. */
387
- export function writeLinkCache(file, home, targets) {
388
- if (!existsSync(dirname(file)))
389
- return;
390
- const aside = `${file}.${process.pid}.part`;
383
+ /** A finished child's lines as the check's answer: `at\t<root>` before each walk, `done\t<answer>` once at the end. */
384
+ export function readLook(ran) {
385
+ const lines = ran.stdout.split("\n");
386
+ const last = [...lines].reverse().find(line => line.startsWith("at\t"));
387
+ const at = last === undefined ? null : (() => { try {
388
+ const root = JSON.parse(last.slice(3));
389
+ return typeof root === "string" && isAbsolute(root) ? root : null;
390
+ }
391
+ catch {
392
+ return null;
393
+ } })();
394
+ if (ran.outcome === "timed-out")
395
+ return { kind: "timed-out", at };
396
+ if (ran.outcome === "failed")
397
+ return { kind: "failed", detail: ran.detail };
398
+ const done = lines.find(line => line.startsWith("done\t"));
399
+ if (done === undefined)
400
+ return { kind: "failed", detail: "the check gave no answer" };
391
401
  try {
392
- writeFileSync(aside, JSON.stringify({ version: 1, home, targets: [...new Set(targets)].sort() }), { mode: 0o600, flag: "wx" });
393
- renameSync(aside, file);
402
+ const value = JSON.parse(done.slice(5));
403
+ const paths = (one) => Array.isArray(one) && one.every(path => typeof path === "string" && isAbsolute(path)) ? one : null;
404
+ const names = paths(value["names"]), denies = paths(value["denies"]), unfinished = paths(value["unfinished"]);
405
+ if (names === null || denies === null || unfinished === null)
406
+ return { kind: "failed", detail: "the check's answer can't be read" };
407
+ return { kind: "done", storage: { names, denies }, unfinished };
394
408
  }
395
409
  catch {
396
- rmSync(aside, { force: true });
410
+ return { kind: "failed", detail: "the check's answer can't be read" };
397
411
  }
398
412
  }
413
+ /** Run a child without waiting on this thread: its output, bounded, and how it ended. Killed at `limitMs`. */
414
+ export function spawnLook(command, args, limitMs) {
415
+ return new Promise(settle => {
416
+ let stdout = "", over = false, timedOut = false, settled = false;
417
+ const finish = (outcome, detail) => { if (settled)
418
+ return; settled = true; clearTimeout(timer); settle({ stdout, outcome, detail }); };
419
+ let child;
420
+ try {
421
+ child = spawn(command, [...args], { stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" } });
422
+ }
423
+ catch (error) {
424
+ settle({ stdout: "", outcome: "failed", detail: error instanceof Error ? error.message : String(error) });
425
+ return;
426
+ }
427
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, limitMs);
428
+ child.stdout?.setEncoding("utf8");
429
+ child.stdout?.on("data", (chunk) => { if (stdout.length + chunk.length > LOOK_OUTPUT_MAX) {
430
+ over = true;
431
+ child.kill("SIGKILL");
432
+ }
433
+ else
434
+ stdout += chunk; });
435
+ child.on("error", error => finish("failed", error.message));
436
+ child.on("close", (code, signal) => {
437
+ if (timedOut)
438
+ finish("timed-out", "");
439
+ else if (over)
440
+ finish("failed", "the check said too much");
441
+ else if (code === 0 && signal === null)
442
+ finish("done", "");
443
+ else
444
+ finish("failed", signal === null ? `the check exited with ${code}` : `the check was stopped (${signal})`);
445
+ });
446
+ });
447
+ }
@@ -137,7 +137,7 @@ function notifyOperators(store, key, message, now) {
137
137
  dedupeKey: `${SIGN_IN_KEY_PREFIX}${key}:${recipient}`,
138
138
  kind: message.kind,
139
139
  pushClass: "attention",
140
- link: "/work",
140
+ link: "/tasks",
141
141
  subject: message.subject,
142
142
  body: message.body,
143
143
  recipient,
@@ -0,0 +1,34 @@
1
+ import type { Store } from "./store.js";
2
+ export declare const CAPTURE_TEXT_LIMIT = 4000;
3
+ export type ProviderCapture = {
4
+ id: number;
5
+ run: number;
6
+ repo: string | null;
7
+ provider: string;
8
+ model: string | null;
9
+ authMode: string | null;
10
+ exitCode: number | null;
11
+ code: string | null;
12
+ text: string;
13
+ hint: boolean;
14
+ label: "exhausted" | "not-exhausted" | null;
15
+ labelledBy: string | null;
16
+ at: string;
17
+ };
18
+ /** Save one failed attempt's last words. Bookkeeping: it never costs the run anything. */
19
+ export declare function captureProviderFailure(store: Store, input: {
20
+ run: number;
21
+ provider: string;
22
+ model: string | null;
23
+ authMode: string | null;
24
+ exitCode: number | null;
25
+ code: string | null;
26
+ texts: readonly (string | null | undefined)[];
27
+ }, now: Date): void;
28
+ /** Newest first, the ones mentioning limits at the top. */
29
+ export declare function providerCaptures(store: Store, repos: readonly string[] | null, limit?: number): ProviderCapture[];
30
+ export declare function providerCapture(store: Store, id: number): ProviderCapture | null;
31
+ /** A person's reading of one capture. It marks it for keeping, and nothing routes on it. */
32
+ export declare function labelProviderCapture(store: Store, id: number, label: "exhausted" | "not-exhausted", by: string): boolean;
33
+ /** What goes in a fixture file: the provider's response and which version of what produced it. */
34
+ export declare function captureFixture(capture: ProviderCapture): Record<string, unknown>;