pi-codex-auto-review 0.1.0

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 (1057) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +10 -0
  3. package/README.md +77 -0
  4. package/dist/approvals.d.ts +72 -0
  5. package/dist/approvals.js +167 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/audit.d.ts +28 -0
  8. package/dist/audit.js +40 -0
  9. package/dist/audit.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +70 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/context-files.d.ts +14 -0
  14. package/dist/context-files.js +113 -0
  15. package/dist/context-files.js.map +1 -0
  16. package/dist/contracts.d.ts +258 -0
  17. package/dist/contracts.js +102 -0
  18. package/dist/contracts.js.map +1 -0
  19. package/dist/index.d.ts +28 -0
  20. package/dist/index.js +106 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/native/darwin-arm64/artifact.json +5 -0
  23. package/dist/native/darwin-arm64/pi-guard-execpolicy +0 -0
  24. package/dist/native/linux-x64/artifact.json +5 -0
  25. package/dist/native/linux-x64/pi-guard-execpolicy +0 -0
  26. package/dist/policy/domains.d.ts +5 -0
  27. package/dist/policy/domains.js +18 -0
  28. package/dist/policy/domains.js.map +1 -0
  29. package/dist/policy/index.d.ts +56 -0
  30. package/dist/policy/index.js +304 -0
  31. package/dist/policy/index.js.map +1 -0
  32. package/dist/policy/paths.d.ts +5 -0
  33. package/dist/policy/paths.js +54 -0
  34. package/dist/policy/paths.js.map +1 -0
  35. package/dist/policy/rules.d.ts +36 -0
  36. package/dist/policy/rules.js +61 -0
  37. package/dist/policy/rules.js.map +1 -0
  38. package/dist/policy/shell.d.ts +14 -0
  39. package/dist/policy/shell.js +122 -0
  40. package/dist/policy/shell.js.map +1 -0
  41. package/dist/reports.d.ts +21 -0
  42. package/dist/reports.js +42 -0
  43. package/dist/reports.js.map +1 -0
  44. package/dist/review/context.d.ts +25 -0
  45. package/dist/review/context.js +108 -0
  46. package/dist/review/context.js.map +1 -0
  47. package/dist/review/investigation.d.ts +39 -0
  48. package/dist/review/investigation.js +36 -0
  49. package/dist/review/investigation.js.map +1 -0
  50. package/dist/review/lifecycle.d.ts +29 -0
  51. package/dist/review/lifecycle.js +71 -0
  52. package/dist/review/lifecycle.js.map +1 -0
  53. package/dist/review/policy.d.ts +4 -0
  54. package/dist/review/policy.js +15 -0
  55. package/dist/review/policy.js.map +1 -0
  56. package/dist/review/upstream-policy.d.ts +2 -0
  57. package/dist/review/upstream-policy.js +210 -0
  58. package/dist/review/upstream-policy.js.map +1 -0
  59. package/dist/reviewer.d.ts +45 -0
  60. package/dist/reviewer.js +148 -0
  61. package/dist/reviewer.js.map +1 -0
  62. package/dist/sandbox/broker.d.ts +1 -0
  63. package/dist/sandbox/broker.js +135 -0
  64. package/dist/sandbox/broker.js.map +1 -0
  65. package/dist/sandbox/config.d.ts +5 -0
  66. package/dist/sandbox/config.js +91 -0
  67. package/dist/sandbox/config.js.map +1 -0
  68. package/dist/sandbox/executor.d.ts +20 -0
  69. package/dist/sandbox/executor.js +144 -0
  70. package/dist/sandbox/executor.js.map +1 -0
  71. package/dist/sandbox/hard-links.d.ts +4 -0
  72. package/dist/sandbox/hard-links.js +145 -0
  73. package/dist/sandbox/hard-links.js.map +1 -0
  74. package/dist/sandbox/linux-read-paths.d.ts +7 -0
  75. package/dist/sandbox/linux-read-paths.js +47 -0
  76. package/dist/sandbox/linux-read-paths.js.map +1 -0
  77. package/dist/sandbox/paths.d.ts +3 -0
  78. package/dist/sandbox/paths.js +10 -0
  79. package/dist/sandbox/paths.js.map +1 -0
  80. package/dist/sandbox/runtime-write-paths.d.ts +2 -0
  81. package/dist/sandbox/runtime-write-paths.js +8 -0
  82. package/dist/sandbox/runtime-write-paths.js.map +1 -0
  83. package/dist/sandbox/seccomp.d.ts +5 -0
  84. package/dist/sandbox/seccomp.js +16 -0
  85. package/dist/sandbox/seccomp.js.map +1 -0
  86. package/dist/sandbox/worker.d.ts +1 -0
  87. package/dist/sandbox/worker.js +73 -0
  88. package/dist/sandbox/worker.js.map +1 -0
  89. package/dist/signals.d.ts +10 -0
  90. package/dist/signals.js +21 -0
  91. package/dist/signals.js.map +1 -0
  92. package/dist/startup.d.ts +22 -0
  93. package/dist/startup.js +100 -0
  94. package/dist/startup.js.map +1 -0
  95. package/dist/tools/controller.d.ts +63 -0
  96. package/dist/tools/controller.js +286 -0
  97. package/dist/tools/controller.js.map +1 -0
  98. package/dist/tools/external.d.ts +30 -0
  99. package/dist/tools/external.js +24 -0
  100. package/dist/tools/external.js.map +1 -0
  101. package/dist/tools/mcp.d.ts +17 -0
  102. package/dist/tools/mcp.js +186 -0
  103. package/dist/tools/mcp.js.map +1 -0
  104. package/docs/publishing.md +73 -0
  105. package/docs/security/dependencies.md +43 -0
  106. package/docs/testing/auto-review-protection.md +130 -0
  107. package/docs/usage.md +217 -0
  108. package/native/execpolicy/LICENSE +201 -0
  109. package/native/execpolicy/NOTICE +6 -0
  110. package/node_modules/@anthropic-ai/sandbox-runtime/LICENSE +201 -0
  111. package/node_modules/@anthropic-ai/sandbox-runtime/README.md +901 -0
  112. package/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.d.ts +3 -0
  113. package/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.d.ts.map +1 -0
  114. package/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js +472 -0
  115. package/node_modules/@anthropic-ai/sandbox-runtime/dist/cli.js.map +1 -0
  116. package/node_modules/@anthropic-ai/sandbox-runtime/dist/index.d.ts +19 -0
  117. package/node_modules/@anthropic-ai/sandbox-runtime/dist/index.d.ts.map +1 -0
  118. package/node_modules/@anthropic-ai/sandbox-runtime/dist/index.js +17 -0
  119. package/node_modules/@anthropic-ai/sandbox-runtime/dist/index.js.map +1 -0
  120. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/address.d.ts +52 -0
  121. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/address.d.ts.map +1 -0
  122. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/address.js +153 -0
  123. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/address.js.map +1 -0
  124. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/aws-sigv4.d.ts +164 -0
  125. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/aws-sigv4.d.ts.map +1 -0
  126. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/aws-sigv4.js +246 -0
  127. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/aws-sigv4.js.map +1 -0
  128. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/body-substitution.d.ts +58 -0
  129. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/body-substitution.d.ts.map +1 -0
  130. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/body-substitution.js +128 -0
  131. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/body-substitution.js.map +1 -0
  132. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-aws-pairs.d.ts +124 -0
  133. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-aws-pairs.d.ts.map +1 -0
  134. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-aws-pairs.js +258 -0
  135. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-aws-pairs.js.map +1 -0
  136. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-decode.d.ts +87 -0
  137. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-decode.d.ts.map +1 -0
  138. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-decode.js +138 -0
  139. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-decode.js.map +1 -0
  140. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-extract.d.ts +67 -0
  141. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-extract.d.ts.map +1 -0
  142. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-extract.js +113 -0
  143. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-extract.js.map +1 -0
  144. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-env.d.ts +70 -0
  145. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-env.d.ts.map +1 -0
  146. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-env.js +171 -0
  147. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-env.js.map +1 -0
  148. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-files.d.ts +120 -0
  149. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-files.d.ts.map +1 -0
  150. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-files.js +281 -0
  151. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-mask-files.js.map +1 -0
  152. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-sentinel.d.ts +108 -0
  153. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-sentinel.d.ts.map +1 -0
  154. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-sentinel.js +208 -0
  155. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/credential-sentinel.js.map +1 -0
  156. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/domain-pattern.d.ts +82 -0
  157. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/domain-pattern.d.ts.map +1 -0
  158. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/domain-pattern.js +155 -0
  159. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/domain-pattern.js.map +1 -0
  160. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/generate-seccomp-filter.d.ts +44 -0
  161. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/generate-seccomp-filter.d.ts.map +1 -0
  162. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/generate-seccomp-filter.js +220 -0
  163. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/generate-seccomp-filter.js.map +1 -0
  164. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/http-proxy.d.ts +150 -0
  165. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/http-proxy.d.ts.map +1 -0
  166. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/http-proxy.js +678 -0
  167. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/http-proxy.js.map +1 -0
  168. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/java-proxy-agent.d.ts +32 -0
  169. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/java-proxy-agent.d.ts.map +1 -0
  170. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/java-proxy-agent.js +111 -0
  171. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/java-proxy-agent.js.map +1 -0
  172. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.d.ts +305 -0
  173. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -0
  174. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.js +2605 -0
  175. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-sandbox-utils.js.map +1 -0
  176. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-violation-monitor.d.ts +55 -0
  177. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-violation-monitor.d.ts.map +1 -0
  178. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-violation-monitor.js +152 -0
  179. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/linux-violation-monitor.js.map +1 -0
  180. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/listen-in-range.d.ts +12 -0
  181. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/listen-in-range.d.ts.map +1 -0
  182. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/listen-in-range.js +43 -0
  183. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/listen-in-range.js.map +1 -0
  184. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.d.ts +116 -0
  185. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -0
  186. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js +1195 -0
  187. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js.map +1 -0
  188. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-ca.d.ts +200 -0
  189. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-ca.d.ts.map +1 -0
  190. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-ca.js +443 -0
  191. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-ca.js.map +1 -0
  192. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-leaf.d.ts +28 -0
  193. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-leaf.d.ts.map +1 -0
  194. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-leaf.js +114 -0
  195. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mitm-leaf.js.map +1 -0
  196. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mux-proxy.d.ts +59 -0
  197. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mux-proxy.d.ts.map +1 -0
  198. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mux-proxy.js +159 -0
  199. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/mux-proxy.js.map +1 -0
  200. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/parent-proxy.d.ts +147 -0
  201. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/parent-proxy.d.ts.map +1 -0
  202. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/parent-proxy.js +445 -0
  203. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/parent-proxy.js.map +1 -0
  204. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/read-deny-glob.d.ts +17 -0
  205. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/read-deny-glob.d.ts.map +1 -0
  206. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/read-deny-glob.js +163 -0
  207. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/read-deny-glob.js.map +1 -0
  208. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/request-filter.d.ts +83 -0
  209. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/request-filter.d.ts.map +1 -0
  210. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/request-filter.js +212 -0
  211. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/request-filter.js.map +1 -0
  212. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/resolved-address-guard.d.ts +76 -0
  213. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/resolved-address-guard.d.ts.map +1 -0
  214. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/resolved-address-guard.js +221 -0
  215. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/resolved-address-guard.js.map +1 -0
  216. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-config.d.ts +1691 -0
  217. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-config.d.ts.map +1 -0
  218. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-config.js +1233 -0
  219. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-config.js.map +1 -0
  220. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-manager.d.ts +97 -0
  221. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-manager.d.ts.map +1 -0
  222. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-manager.js +1937 -0
  223. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-manager.js.map +1 -0
  224. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-schemas.d.ts +113 -0
  225. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-schemas.d.ts.map +1 -0
  226. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-schemas.js +3 -0
  227. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-schemas.js.map +1 -0
  228. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.d.ts +411 -0
  229. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.d.ts.map +1 -0
  230. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js +1408 -0
  231. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js.map +1 -0
  232. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-violation-store.d.ts +37 -0
  233. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-violation-store.d.ts.map +1 -0
  234. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-violation-store.js +125 -0
  235. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-violation-store.js.map +1 -0
  236. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/socks-proxy.d.ts +57 -0
  237. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/socks-proxy.d.ts.map +1 -0
  238. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/socks-proxy.js +332 -0
  239. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/socks-proxy.js.map +1 -0
  240. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/tls-terminate-proxy.d.ts +100 -0
  241. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -0
  242. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/tls-terminate-proxy.js +546 -0
  243. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/tls-terminate-proxy.js.map +1 -0
  244. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/windows-sandbox-utils.d.ts +923 -0
  245. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -0
  246. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/windows-sandbox-utils.js +1419 -0
  247. package/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/windows-sandbox-utils.js.map +1 -0
  248. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/config-loader.d.ts +31 -0
  249. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/config-loader.d.ts.map +1 -0
  250. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/config-loader.js +69 -0
  251. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/config-loader.js.map +1 -0
  252. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/debug.d.ts +7 -0
  253. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/debug.d.ts.map +1 -0
  254. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/debug.js +25 -0
  255. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/debug.js.map +1 -0
  256. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/platform.d.ts +15 -0
  257. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/platform.d.ts.map +1 -0
  258. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/platform.js +49 -0
  259. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/platform.js.map +1 -0
  260. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/ripgrep.d.ts +17 -0
  261. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/ripgrep.d.ts.map +1 -0
  262. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/ripgrep.js +37 -0
  263. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/ripgrep.js.map +1 -0
  264. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/shell-quote.d.ts +27 -0
  265. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/shell-quote.d.ts.map +1 -0
  266. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/shell-quote.js +47 -0
  267. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/shell-quote.js.map +1 -0
  268. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/which.d.ts +9 -0
  269. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/which.d.ts.map +1 -0
  270. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/which.js +25 -0
  271. package/node_modules/@anthropic-ai/sandbox-runtime/dist/utils/which.js.map +1 -0
  272. package/node_modules/@anthropic-ai/sandbox-runtime/package.json +88 -0
  273. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/java-proxy-agent/build.ts +95 -0
  274. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
  275. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
  276. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/seccomp/build.ts +69 -0
  277. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
  278. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
  279. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/srt-win/build.ts +28 -0
  280. package/node_modules/@anthropic-ai/sandbox-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
  281. package/node_modules/@pondwader/socks5-server/.github/workflows/ci.yml +27 -0
  282. package/node_modules/@pondwader/socks5-server/LICENSE +20 -0
  283. package/node_modules/@pondwader/socks5-server/README.md +151 -0
  284. package/node_modules/@pondwader/socks5-server/dist/index.d.mts +87 -0
  285. package/node_modules/@pondwader/socks5-server/dist/index.d.ts +87 -0
  286. package/node_modules/@pondwader/socks5-server/dist/index.js +346 -0
  287. package/node_modules/@pondwader/socks5-server/dist/index.mjs +307 -0
  288. package/node_modules/@pondwader/socks5-server/package.json +31 -0
  289. package/node_modules/commander/LICENSE +22 -0
  290. package/node_modules/commander/Readme.md +1157 -0
  291. package/node_modules/commander/esm.mjs +16 -0
  292. package/node_modules/commander/index.js +24 -0
  293. package/node_modules/commander/lib/argument.js +149 -0
  294. package/node_modules/commander/lib/command.js +2509 -0
  295. package/node_modules/commander/lib/error.js +39 -0
  296. package/node_modules/commander/lib/help.js +520 -0
  297. package/node_modules/commander/lib/option.js +330 -0
  298. package/node_modules/commander/lib/suggestSimilar.js +101 -0
  299. package/node_modules/commander/package-support.json +16 -0
  300. package/node_modules/commander/package.json +84 -0
  301. package/node_modules/commander/typings/esm.d.mts +3 -0
  302. package/node_modules/commander/typings/index.d.ts +969 -0
  303. package/node_modules/node-forge/.editorconfig +17 -0
  304. package/node_modules/node-forge/.eslintrc.js +24 -0
  305. package/node_modules/node-forge/.github/workflows/main.yaml +96 -0
  306. package/node_modules/node-forge/.istanbul.yml +2 -0
  307. package/node_modules/node-forge/.npmignore +15 -0
  308. package/node_modules/node-forge/CHANGELOG.md +555 -0
  309. package/node_modules/node-forge/CONTRIBUTING.md +18 -0
  310. package/node_modules/node-forge/LICENSE +331 -0
  311. package/node_modules/node-forge/README.md +2071 -0
  312. package/node_modules/node-forge/RELEASE.md +19 -0
  313. package/node_modules/node-forge/SECURITY.md +5 -0
  314. package/node_modules/node-forge/examples/create-cert.js +110 -0
  315. package/node_modules/node-forge/examples/create-csr.js +66 -0
  316. package/node_modules/node-forge/examples/create-pkcs12.js +160 -0
  317. package/node_modules/node-forge/examples/imap.js +46 -0
  318. package/node_modules/node-forge/examples/sign-p7.js +115 -0
  319. package/node_modules/node-forge/examples/tls.js +189 -0
  320. package/node_modules/node-forge/flash/PooledSocket.as +18 -0
  321. package/node_modules/node-forge/flash/README.md +48 -0
  322. package/node_modules/node-forge/flash/SocketEvent.as +36 -0
  323. package/node_modules/node-forge/flash/SocketPool.as +754 -0
  324. package/node_modules/node-forge/flash/mod_fsp/README +135 -0
  325. package/node_modules/node-forge/flash/mod_fsp/mod_fsp.c +415 -0
  326. package/node_modules/node-forge/flash/package.json +28 -0
  327. package/node_modules/node-forge/flash/policyserver.js +65 -0
  328. package/node_modules/node-forge/flash/policyserver.py +112 -0
  329. package/node_modules/node-forge/flash/swf/SocketPool.swf +0 -0
  330. package/node_modules/node-forge/karma-sauce.conf.js +79 -0
  331. package/node_modules/node-forge/karma.conf.js +133 -0
  332. package/node_modules/node-forge/lib/aes.js +1091 -0
  333. package/node_modules/node-forge/lib/aesCipherSuites.js +282 -0
  334. package/node_modules/node-forge/lib/asn1-validator.js +91 -0
  335. package/node_modules/node-forge/lib/asn1.js +1503 -0
  336. package/node_modules/node-forge/lib/baseN.js +186 -0
  337. package/node_modules/node-forge/lib/cipher.js +230 -0
  338. package/node_modules/node-forge/lib/cipherModes.js +999 -0
  339. package/node_modules/node-forge/lib/des.js +496 -0
  340. package/node_modules/node-forge/lib/ed25519.js +1091 -0
  341. package/node_modules/node-forge/lib/forge.js +13 -0
  342. package/node_modules/node-forge/lib/form.js +149 -0
  343. package/node_modules/node-forge/lib/hmac.js +146 -0
  344. package/node_modules/node-forge/lib/http.js +1346 -0
  345. package/node_modules/node-forge/lib/index.all.js +16 -0
  346. package/node_modules/node-forge/lib/index.js +33 -0
  347. package/node_modules/node-forge/lib/jsbn.js +1294 -0
  348. package/node_modules/node-forge/lib/kem.js +168 -0
  349. package/node_modules/node-forge/lib/log.js +319 -0
  350. package/node_modules/node-forge/lib/md.all.js +13 -0
  351. package/node_modules/node-forge/lib/md.js +11 -0
  352. package/node_modules/node-forge/lib/md5.js +289 -0
  353. package/node_modules/node-forge/lib/mgf.js +12 -0
  354. package/node_modules/node-forge/lib/mgf1.js +57 -0
  355. package/node_modules/node-forge/lib/oids.js +180 -0
  356. package/node_modules/node-forge/lib/pbe.js +1023 -0
  357. package/node_modules/node-forge/lib/pbkdf2.js +211 -0
  358. package/node_modules/node-forge/lib/pem.js +237 -0
  359. package/node_modules/node-forge/lib/pkcs1.js +276 -0
  360. package/node_modules/node-forge/lib/pkcs12.js +1078 -0
  361. package/node_modules/node-forge/lib/pkcs7.js +1260 -0
  362. package/node_modules/node-forge/lib/pkcs7asn1.js +410 -0
  363. package/node_modules/node-forge/lib/pki.js +102 -0
  364. package/node_modules/node-forge/lib/prime.js +297 -0
  365. package/node_modules/node-forge/lib/prime.worker.js +168 -0
  366. package/node_modules/node-forge/lib/prng.js +419 -0
  367. package/node_modules/node-forge/lib/pss.js +241 -0
  368. package/node_modules/node-forge/lib/random.js +191 -0
  369. package/node_modules/node-forge/lib/rc2.js +410 -0
  370. package/node_modules/node-forge/lib/rsa.js +1973 -0
  371. package/node_modules/node-forge/lib/sha1.js +319 -0
  372. package/node_modules/node-forge/lib/sha256.js +327 -0
  373. package/node_modules/node-forge/lib/sha512.js +561 -0
  374. package/node_modules/node-forge/lib/socket.js +287 -0
  375. package/node_modules/node-forge/lib/ssh.js +236 -0
  376. package/node_modules/node-forge/lib/tls.js +4282 -0
  377. package/node_modules/node-forge/lib/tlssocket.js +249 -0
  378. package/node_modules/node-forge/lib/util.js +2652 -0
  379. package/node_modules/node-forge/lib/x509.js +3251 -0
  380. package/node_modules/node-forge/lib/xhr.js +738 -0
  381. package/node_modules/node-forge/package.json +123 -0
  382. package/node_modules/node-forge/tests/.eslintrc.js +5 -0
  383. package/node_modules/node-forge/tests/README.md +4 -0
  384. package/node_modules/node-forge/tests/benchmarks/aes-speed.js +62 -0
  385. package/node_modules/node-forge/tests/benchmarks/so-44303784.js +246 -0
  386. package/node_modules/node-forge/tests/flash/README.md +7 -0
  387. package/node_modules/node-forge/tests/flash/Test.as +96 -0
  388. package/node_modules/node-forge/tests/flash/build-flash.xml +7 -0
  389. package/node_modules/node-forge/tests/flash/index.html +28 -0
  390. package/node_modules/node-forge/tests/index.html +33 -0
  391. package/node_modules/node-forge/tests/issues/heartbleed.js +55 -0
  392. package/node_modules/node-forge/tests/issues/issue-428.html +95 -0
  393. package/node_modules/node-forge/tests/issues/issue-428.js +375 -0
  394. package/node_modules/node-forge/tests/karma/index.js +8 -0
  395. package/node_modules/node-forge/tests/karma/testWorker.js +65 -0
  396. package/node_modules/node-forge/tests/karma/web-worker-rsa.js +55 -0
  397. package/node_modules/node-forge/tests/legacy/common.html +65 -0
  398. package/node_modules/node-forge/tests/legacy/common.js +2199 -0
  399. package/node_modules/node-forge/tests/legacy/form.html +148 -0
  400. package/node_modules/node-forge/tests/legacy/form.js +40 -0
  401. package/node_modules/node-forge/tests/legacy/http.html +235 -0
  402. package/node_modules/node-forge/tests/legacy/index.html +70 -0
  403. package/node_modules/node-forge/tests/legacy/keygen.html +113 -0
  404. package/node_modules/node-forge/tests/legacy/login.css +26 -0
  405. package/node_modules/node-forge/tests/legacy/loginDemo.html +32 -0
  406. package/node_modules/node-forge/tests/legacy/loginDemo.js +149 -0
  407. package/node_modules/node-forge/tests/legacy/performance.html +533 -0
  408. package/node_modules/node-forge/tests/legacy/result.txt +1 -0
  409. package/node_modules/node-forge/tests/legacy/socketPool.html +302 -0
  410. package/node_modules/node-forge/tests/legacy/tasks.html +54 -0
  411. package/node_modules/node-forge/tests/legacy/tasks.js +378 -0
  412. package/node_modules/node-forge/tests/legacy/tls.html +389 -0
  413. package/node_modules/node-forge/tests/legacy/webid.html +89 -0
  414. package/node_modules/node-forge/tests/legacy/webid.js +313 -0
  415. package/node_modules/node-forge/tests/legacy/ws-webid.js +120 -0
  416. package/node_modules/node-forge/tests/legacy/xhr.html +67 -0
  417. package/node_modules/node-forge/tests/legacy/xhr.js +590 -0
  418. package/node_modules/node-forge/tests/pocs/ghsa-2328-f5f3-gj25.js +85 -0
  419. package/node_modules/node-forge/tests/pocs/ghsa-ppp5-5v6c-4jwp.js +166 -0
  420. package/node_modules/node-forge/tests/pocs/ghsa-q67f-28xg-22rw.js +103 -0
  421. package/node_modules/node-forge/tests/security/cve-2025-12816.js +75 -0
  422. package/node_modules/node-forge/tests/security/ghsa-554w-wpv2-vw27.js +81 -0
  423. package/node_modules/node-forge/tests/security/index.js +3 -0
  424. package/node_modules/node-forge/tests/server.crt +26 -0
  425. package/node_modules/node-forge/tests/server.js +87 -0
  426. package/node_modules/node-forge/tests/server.key +27 -0
  427. package/node_modules/node-forge/tests/support/task.js +718 -0
  428. package/node_modules/node-forge/tests/tests.css +61 -0
  429. package/node_modules/node-forge/tests/unit/aes.js +1498 -0
  430. package/node_modules/node-forge/tests/unit/asn1.js +1337 -0
  431. package/node_modules/node-forge/tests/unit/csr.js +129 -0
  432. package/node_modules/node-forge/tests/unit/des.js +231 -0
  433. package/node_modules/node-forge/tests/unit/ed25519.js +395 -0
  434. package/node_modules/node-forge/tests/unit/forge.js +2 -0
  435. package/node_modules/node-forge/tests/unit/hmac.js +64 -0
  436. package/node_modules/node-forge/tests/unit/index.html +20 -0
  437. package/node_modules/node-forge/tests/unit/index.js +27 -0
  438. package/node_modules/node-forge/tests/unit/jsbn.js +40 -0
  439. package/node_modules/node-forge/tests/unit/kem.js +167 -0
  440. package/node_modules/node-forge/tests/unit/md5.js +114 -0
  441. package/node_modules/node-forge/tests/unit/mgf1.js +16 -0
  442. package/node_modules/node-forge/tests/unit/pbkdf2.js +182 -0
  443. package/node_modules/node-forge/tests/unit/pem.js +159 -0
  444. package/node_modules/node-forge/tests/unit/pkcs1.js +1077 -0
  445. package/node_modules/node-forge/tests/unit/pkcs12.js +739 -0
  446. package/node_modules/node-forge/tests/unit/pkcs7.js +792 -0
  447. package/node_modules/node-forge/tests/unit/random.js +48 -0
  448. package/node_modules/node-forge/tests/unit/rc2.js +88 -0
  449. package/node_modules/node-forge/tests/unit/rsa.js +1278 -0
  450. package/node_modules/node-forge/tests/unit/sha1.js +72 -0
  451. package/node_modules/node-forge/tests/unit/sha256.js +79 -0
  452. package/node_modules/node-forge/tests/unit/sha512.js +200 -0
  453. package/node_modules/node-forge/tests/unit/ssh.js +175 -0
  454. package/node_modules/node-forge/tests/unit/tls.js +175 -0
  455. package/node_modules/node-forge/tests/unit/util.js +595 -0
  456. package/node_modules/node-forge/tests/unit/x509.js +1986 -0
  457. package/node_modules/node-forge/tests/websockets/server-webid.js +495 -0
  458. package/node_modules/node-forge/tests/websockets/server-ws.js +158 -0
  459. package/node_modules/node-forge/webpack-tests.config.js +26 -0
  460. package/node_modules/node-forge/webpack.config.js +130 -0
  461. package/node_modules/zod/LICENSE +21 -0
  462. package/node_modules/zod/README.md +208 -0
  463. package/node_modules/zod/index.cjs +33 -0
  464. package/node_modules/zod/index.d.cts +4 -0
  465. package/node_modules/zod/index.d.ts +4 -0
  466. package/node_modules/zod/index.js +4 -0
  467. package/node_modules/zod/package.json +118 -0
  468. package/node_modules/zod/src/index.ts +4 -0
  469. package/node_modules/zod/src/v3/ZodError.ts +330 -0
  470. package/node_modules/zod/src/v3/benchmarks/datetime.ts +58 -0
  471. package/node_modules/zod/src/v3/benchmarks/discriminatedUnion.ts +80 -0
  472. package/node_modules/zod/src/v3/benchmarks/index.ts +59 -0
  473. package/node_modules/zod/src/v3/benchmarks/ipv4.ts +57 -0
  474. package/node_modules/zod/src/v3/benchmarks/object.ts +69 -0
  475. package/node_modules/zod/src/v3/benchmarks/primitives.ts +162 -0
  476. package/node_modules/zod/src/v3/benchmarks/realworld.ts +63 -0
  477. package/node_modules/zod/src/v3/benchmarks/string.ts +55 -0
  478. package/node_modules/zod/src/v3/benchmarks/union.ts +80 -0
  479. package/node_modules/zod/src/v3/errors.ts +13 -0
  480. package/node_modules/zod/src/v3/external.ts +6 -0
  481. package/node_modules/zod/src/v3/helpers/enumUtil.ts +17 -0
  482. package/node_modules/zod/src/v3/helpers/errorUtil.ts +8 -0
  483. package/node_modules/zod/src/v3/helpers/parseUtil.ts +176 -0
  484. package/node_modules/zod/src/v3/helpers/partialUtil.ts +34 -0
  485. package/node_modules/zod/src/v3/helpers/typeAliases.ts +2 -0
  486. package/node_modules/zod/src/v3/helpers/util.ts +224 -0
  487. package/node_modules/zod/src/v3/index.ts +4 -0
  488. package/node_modules/zod/src/v3/locales/en.ts +124 -0
  489. package/node_modules/zod/src/v3/standard-schema.ts +113 -0
  490. package/node_modules/zod/src/v3/tests/Mocker.ts +54 -0
  491. package/node_modules/zod/src/v3/tests/all-errors.test.ts +157 -0
  492. package/node_modules/zod/src/v3/tests/anyunknown.test.ts +28 -0
  493. package/node_modules/zod/src/v3/tests/array.test.ts +71 -0
  494. package/node_modules/zod/src/v3/tests/async-parsing.test.ts +388 -0
  495. package/node_modules/zod/src/v3/tests/async-refinements.test.ts +46 -0
  496. package/node_modules/zod/src/v3/tests/base.test.ts +29 -0
  497. package/node_modules/zod/src/v3/tests/bigint.test.ts +55 -0
  498. package/node_modules/zod/src/v3/tests/branded.test.ts +53 -0
  499. package/node_modules/zod/src/v3/tests/catch.test.ts +220 -0
  500. package/node_modules/zod/src/v3/tests/coerce.test.ts +133 -0
  501. package/node_modules/zod/src/v3/tests/complex.test.ts +56 -0
  502. package/node_modules/zod/src/v3/tests/custom.test.ts +31 -0
  503. package/node_modules/zod/src/v3/tests/date.test.ts +32 -0
  504. package/node_modules/zod/src/v3/tests/deepmasking.test.ts +186 -0
  505. package/node_modules/zod/src/v3/tests/default.test.ts +112 -0
  506. package/node_modules/zod/src/v3/tests/description.test.ts +33 -0
  507. package/node_modules/zod/src/v3/tests/discriminated-unions.test.ts +315 -0
  508. package/node_modules/zod/src/v3/tests/enum.test.ts +80 -0
  509. package/node_modules/zod/src/v3/tests/error.test.ts +551 -0
  510. package/node_modules/zod/src/v3/tests/firstparty.test.ts +87 -0
  511. package/node_modules/zod/src/v3/tests/firstpartyschematypes.test.ts +21 -0
  512. package/node_modules/zod/src/v3/tests/function.test.ts +257 -0
  513. package/node_modules/zod/src/v3/tests/generics.test.ts +48 -0
  514. package/node_modules/zod/src/v3/tests/instanceof.test.ts +37 -0
  515. package/node_modules/zod/src/v3/tests/intersection.test.ts +110 -0
  516. package/node_modules/zod/src/v3/tests/language-server.source.ts +76 -0
  517. package/node_modules/zod/src/v3/tests/language-server.test.ts +207 -0
  518. package/node_modules/zod/src/v3/tests/literal.test.ts +36 -0
  519. package/node_modules/zod/src/v3/tests/map.test.ts +110 -0
  520. package/node_modules/zod/src/v3/tests/masking.test.ts +4 -0
  521. package/node_modules/zod/src/v3/tests/mocker.test.ts +19 -0
  522. package/node_modules/zod/src/v3/tests/nan.test.ts +21 -0
  523. package/node_modules/zod/src/v3/tests/nativeEnum.test.ts +87 -0
  524. package/node_modules/zod/src/v3/tests/nullable.test.ts +42 -0
  525. package/node_modules/zod/src/v3/tests/number.test.ts +176 -0
  526. package/node_modules/zod/src/v3/tests/object-augmentation.test.ts +29 -0
  527. package/node_modules/zod/src/v3/tests/object-in-es5-env.test.ts +29 -0
  528. package/node_modules/zod/src/v3/tests/object.test.ts +434 -0
  529. package/node_modules/zod/src/v3/tests/optional.test.ts +42 -0
  530. package/node_modules/zod/src/v3/tests/parseUtil.test.ts +23 -0
  531. package/node_modules/zod/src/v3/tests/parser.test.ts +41 -0
  532. package/node_modules/zod/src/v3/tests/partials.test.ts +243 -0
  533. package/node_modules/zod/src/v3/tests/pickomit.test.ts +111 -0
  534. package/node_modules/zod/src/v3/tests/pipeline.test.ts +29 -0
  535. package/node_modules/zod/src/v3/tests/preprocess.test.ts +186 -0
  536. package/node_modules/zod/src/v3/tests/primitive.test.ts +440 -0
  537. package/node_modules/zod/src/v3/tests/promise.test.ts +90 -0
  538. package/node_modules/zod/src/v3/tests/readonly.test.ts +194 -0
  539. package/node_modules/zod/src/v3/tests/record.test.ts +171 -0
  540. package/node_modules/zod/src/v3/tests/recursive.test.ts +197 -0
  541. package/node_modules/zod/src/v3/tests/refine.test.ts +313 -0
  542. package/node_modules/zod/src/v3/tests/safeparse.test.ts +27 -0
  543. package/node_modules/zod/src/v3/tests/set.test.ts +142 -0
  544. package/node_modules/zod/src/v3/tests/standard-schema.test.ts +83 -0
  545. package/node_modules/zod/src/v3/tests/string.test.ts +916 -0
  546. package/node_modules/zod/src/v3/tests/transformer.test.ts +233 -0
  547. package/node_modules/zod/src/v3/tests/tuple.test.ts +90 -0
  548. package/node_modules/zod/src/v3/tests/unions.test.ts +57 -0
  549. package/node_modules/zod/src/v3/tests/validations.test.ts +133 -0
  550. package/node_modules/zod/src/v3/tests/void.test.ts +15 -0
  551. package/node_modules/zod/src/v3/types.ts +5136 -0
  552. package/node_modules/zod/src/v4/classic/checks.ts +30 -0
  553. package/node_modules/zod/src/v4/classic/coerce.ts +27 -0
  554. package/node_modules/zod/src/v4/classic/compat.ts +66 -0
  555. package/node_modules/zod/src/v4/classic/errors.ts +75 -0
  556. package/node_modules/zod/src/v4/classic/external.ts +50 -0
  557. package/node_modules/zod/src/v4/classic/index.ts +5 -0
  558. package/node_modules/zod/src/v4/classic/iso.ts +90 -0
  559. package/node_modules/zod/src/v4/classic/parse.ts +33 -0
  560. package/node_modules/zod/src/v4/classic/schemas.ts +2054 -0
  561. package/node_modules/zod/src/v4/classic/tests/anyunknown.test.ts +26 -0
  562. package/node_modules/zod/src/v4/classic/tests/array.test.ts +264 -0
  563. package/node_modules/zod/src/v4/classic/tests/assignability.test.ts +210 -0
  564. package/node_modules/zod/src/v4/classic/tests/async-parsing.test.ts +381 -0
  565. package/node_modules/zod/src/v4/classic/tests/async-refinements.test.ts +68 -0
  566. package/node_modules/zod/src/v4/classic/tests/base.test.ts +7 -0
  567. package/node_modules/zod/src/v4/classic/tests/bigint.test.ts +54 -0
  568. package/node_modules/zod/src/v4/classic/tests/brand.test.ts +63 -0
  569. package/node_modules/zod/src/v4/classic/tests/catch.test.ts +252 -0
  570. package/node_modules/zod/src/v4/classic/tests/coalesce.test.ts +20 -0
  571. package/node_modules/zod/src/v4/classic/tests/coerce.test.ts +160 -0
  572. package/node_modules/zod/src/v4/classic/tests/continuability.test.ts +352 -0
  573. package/node_modules/zod/src/v4/classic/tests/custom.test.ts +40 -0
  574. package/node_modules/zod/src/v4/classic/tests/date.test.ts +31 -0
  575. package/node_modules/zod/src/v4/classic/tests/datetime.test.ts +296 -0
  576. package/node_modules/zod/src/v4/classic/tests/default.test.ts +313 -0
  577. package/node_modules/zod/src/v4/classic/tests/description.test.ts +32 -0
  578. package/node_modules/zod/src/v4/classic/tests/discriminated-unions.test.ts +619 -0
  579. package/node_modules/zod/src/v4/classic/tests/enum.test.ts +285 -0
  580. package/node_modules/zod/src/v4/classic/tests/error-utils.test.ts +527 -0
  581. package/node_modules/zod/src/v4/classic/tests/error.test.ts +711 -0
  582. package/node_modules/zod/src/v4/classic/tests/file.test.ts +91 -0
  583. package/node_modules/zod/src/v4/classic/tests/firstparty.test.ts +175 -0
  584. package/node_modules/zod/src/v4/classic/tests/function.test.ts +268 -0
  585. package/node_modules/zod/src/v4/classic/tests/generics.test.ts +72 -0
  586. package/node_modules/zod/src/v4/classic/tests/index.test.ts +829 -0
  587. package/node_modules/zod/src/v4/classic/tests/instanceof.test.ts +34 -0
  588. package/node_modules/zod/src/v4/classic/tests/intersection.test.ts +171 -0
  589. package/node_modules/zod/src/v4/classic/tests/json.test.ts +108 -0
  590. package/node_modules/zod/src/v4/classic/tests/lazy.test.ts +227 -0
  591. package/node_modules/zod/src/v4/classic/tests/literal.test.ts +92 -0
  592. package/node_modules/zod/src/v4/classic/tests/map.test.ts +196 -0
  593. package/node_modules/zod/src/v4/classic/tests/nan.test.ts +21 -0
  594. package/node_modules/zod/src/v4/classic/tests/nested-refine.test.ts +168 -0
  595. package/node_modules/zod/src/v4/classic/tests/nonoptional.test.ts +86 -0
  596. package/node_modules/zod/src/v4/classic/tests/nullable.test.ts +22 -0
  597. package/node_modules/zod/src/v4/classic/tests/number.test.ts +247 -0
  598. package/node_modules/zod/src/v4/classic/tests/object.test.ts +563 -0
  599. package/node_modules/zod/src/v4/classic/tests/optional.test.ts +123 -0
  600. package/node_modules/zod/src/v4/classic/tests/partial.test.ts +147 -0
  601. package/node_modules/zod/src/v4/classic/tests/pickomit.test.ts +127 -0
  602. package/node_modules/zod/src/v4/classic/tests/pipe.test.ts +81 -0
  603. package/node_modules/zod/src/v4/classic/tests/prefault.test.ts +37 -0
  604. package/node_modules/zod/src/v4/classic/tests/preprocess.test.ts +298 -0
  605. package/node_modules/zod/src/v4/classic/tests/primitive.test.ts +175 -0
  606. package/node_modules/zod/src/v4/classic/tests/promise.test.ts +81 -0
  607. package/node_modules/zod/src/v4/classic/tests/prototypes.test.ts +23 -0
  608. package/node_modules/zod/src/v4/classic/tests/readonly.test.ts +252 -0
  609. package/node_modules/zod/src/v4/classic/tests/record.test.ts +342 -0
  610. package/node_modules/zod/src/v4/classic/tests/recursive-types.test.ts +356 -0
  611. package/node_modules/zod/src/v4/classic/tests/refine.test.ts +532 -0
  612. package/node_modules/zod/src/v4/classic/tests/registries.test.ts +204 -0
  613. package/node_modules/zod/src/v4/classic/tests/set.test.ts +179 -0
  614. package/node_modules/zod/src/v4/classic/tests/standard-schema.test.ts +57 -0
  615. package/node_modules/zod/src/v4/classic/tests/string-formats.test.ts +109 -0
  616. package/node_modules/zod/src/v4/classic/tests/string.test.ts +881 -0
  617. package/node_modules/zod/src/v4/classic/tests/stringbool.test.ts +66 -0
  618. package/node_modules/zod/src/v4/classic/tests/template-literal.test.ts +758 -0
  619. package/node_modules/zod/src/v4/classic/tests/to-json-schema.test.ts +2314 -0
  620. package/node_modules/zod/src/v4/classic/tests/transform.test.ts +250 -0
  621. package/node_modules/zod/src/v4/classic/tests/tuple.test.ts +163 -0
  622. package/node_modules/zod/src/v4/classic/tests/union.test.ts +94 -0
  623. package/node_modules/zod/src/v4/classic/tests/validations.test.ts +283 -0
  624. package/node_modules/zod/src/v4/classic/tests/void.test.ts +12 -0
  625. package/node_modules/zod/src/v4/core/api.ts +1594 -0
  626. package/node_modules/zod/src/v4/core/checks.ts +1283 -0
  627. package/node_modules/zod/src/v4/core/config.ts +15 -0
  628. package/node_modules/zod/src/v4/core/core.ts +134 -0
  629. package/node_modules/zod/src/v4/core/doc.ts +44 -0
  630. package/node_modules/zod/src/v4/core/errors.ts +424 -0
  631. package/node_modules/zod/src/v4/core/function.ts +176 -0
  632. package/node_modules/zod/src/v4/core/index.ts +15 -0
  633. package/node_modules/zod/src/v4/core/json-schema.ts +143 -0
  634. package/node_modules/zod/src/v4/core/parse.ts +94 -0
  635. package/node_modules/zod/src/v4/core/regexes.ts +135 -0
  636. package/node_modules/zod/src/v4/core/registries.ts +96 -0
  637. package/node_modules/zod/src/v4/core/schemas.ts +3842 -0
  638. package/node_modules/zod/src/v4/core/standard-schema.ts +64 -0
  639. package/node_modules/zod/src/v4/core/tests/index.test.ts +46 -0
  640. package/node_modules/zod/src/v4/core/tests/locales/be.test.ts +124 -0
  641. package/node_modules/zod/src/v4/core/tests/locales/en.test.ts +22 -0
  642. package/node_modules/zod/src/v4/core/tests/locales/ru.test.ts +128 -0
  643. package/node_modules/zod/src/v4/core/tests/locales/tr.test.ts +69 -0
  644. package/node_modules/zod/src/v4/core/to-json-schema.ts +977 -0
  645. package/node_modules/zod/src/v4/core/util.ts +775 -0
  646. package/node_modules/zod/src/v4/core/versions.ts +5 -0
  647. package/node_modules/zod/src/v4/core/zsf.ts +323 -0
  648. package/node_modules/zod/src/v4/index.ts +4 -0
  649. package/node_modules/zod/src/v4/locales/ar.ts +125 -0
  650. package/node_modules/zod/src/v4/locales/az.ts +121 -0
  651. package/node_modules/zod/src/v4/locales/be.ts +184 -0
  652. package/node_modules/zod/src/v4/locales/ca.ts +127 -0
  653. package/node_modules/zod/src/v4/locales/cs.ts +142 -0
  654. package/node_modules/zod/src/v4/locales/de.ts +124 -0
  655. package/node_modules/zod/src/v4/locales/en.ts +127 -0
  656. package/node_modules/zod/src/v4/locales/eo.ts +125 -0
  657. package/node_modules/zod/src/v4/locales/es.ts +125 -0
  658. package/node_modules/zod/src/v4/locales/fa.ts +134 -0
  659. package/node_modules/zod/src/v4/locales/fi.ts +131 -0
  660. package/node_modules/zod/src/v4/locales/fr-CA.ts +126 -0
  661. package/node_modules/zod/src/v4/locales/fr.ts +124 -0
  662. package/node_modules/zod/src/v4/locales/he.ts +125 -0
  663. package/node_modules/zod/src/v4/locales/hu.ts +126 -0
  664. package/node_modules/zod/src/v4/locales/id.ts +125 -0
  665. package/node_modules/zod/src/v4/locales/index.ts +39 -0
  666. package/node_modules/zod/src/v4/locales/it.ts +125 -0
  667. package/node_modules/zod/src/v4/locales/ja.ts +122 -0
  668. package/node_modules/zod/src/v4/locales/kh.ts +126 -0
  669. package/node_modules/zod/src/v4/locales/ko.ts +131 -0
  670. package/node_modules/zod/src/v4/locales/mk.ts +127 -0
  671. package/node_modules/zod/src/v4/locales/ms.ts +124 -0
  672. package/node_modules/zod/src/v4/locales/nl.ts +126 -0
  673. package/node_modules/zod/src/v4/locales/no.ts +124 -0
  674. package/node_modules/zod/src/v4/locales/ota.ts +125 -0
  675. package/node_modules/zod/src/v4/locales/pl.ts +126 -0
  676. package/node_modules/zod/src/v4/locales/ps.ts +133 -0
  677. package/node_modules/zod/src/v4/locales/pt.ts +123 -0
  678. package/node_modules/zod/src/v4/locales/ru.ts +184 -0
  679. package/node_modules/zod/src/v4/locales/sl.ts +126 -0
  680. package/node_modules/zod/src/v4/locales/sv.ts +127 -0
  681. package/node_modules/zod/src/v4/locales/ta.ts +125 -0
  682. package/node_modules/zod/src/v4/locales/th.ts +126 -0
  683. package/node_modules/zod/src/v4/locales/tr.ts +121 -0
  684. package/node_modules/zod/src/v4/locales/ua.ts +126 -0
  685. package/node_modules/zod/src/v4/locales/ur.ts +126 -0
  686. package/node_modules/zod/src/v4/locales/vi.ts +125 -0
  687. package/node_modules/zod/src/v4/locales/zh-CN.ts +123 -0
  688. package/node_modules/zod/src/v4/locales/zh-TW.ts +125 -0
  689. package/node_modules/zod/src/v4/mini/checks.ts +32 -0
  690. package/node_modules/zod/src/v4/mini/coerce.ts +22 -0
  691. package/node_modules/zod/src/v4/mini/external.ts +40 -0
  692. package/node_modules/zod/src/v4/mini/index.ts +3 -0
  693. package/node_modules/zod/src/v4/mini/iso.ts +62 -0
  694. package/node_modules/zod/src/v4/mini/parse.ts +1 -0
  695. package/node_modules/zod/src/v4/mini/schemas.ts +1579 -0
  696. package/node_modules/zod/src/v4/mini/tests/assignability.test.ts +129 -0
  697. package/node_modules/zod/src/v4/mini/tests/brand.test.ts +51 -0
  698. package/node_modules/zod/src/v4/mini/tests/checks.test.ts +144 -0
  699. package/node_modules/zod/src/v4/mini/tests/computed.test.ts +36 -0
  700. package/node_modules/zod/src/v4/mini/tests/error.test.ts +22 -0
  701. package/node_modules/zod/src/v4/mini/tests/functions.test.ts +43 -0
  702. package/node_modules/zod/src/v4/mini/tests/index.test.ts +871 -0
  703. package/node_modules/zod/src/v4/mini/tests/number.test.ts +95 -0
  704. package/node_modules/zod/src/v4/mini/tests/object.test.ts +185 -0
  705. package/node_modules/zod/src/v4/mini/tests/prototypes.test.ts +43 -0
  706. package/node_modules/zod/src/v4/mini/tests/recursive-types.test.ts +275 -0
  707. package/node_modules/zod/src/v4/mini/tests/string.test.ts +299 -0
  708. package/node_modules/zod/src/v4-mini/index.ts +1 -0
  709. package/node_modules/zod/v3/ZodError.cjs +138 -0
  710. package/node_modules/zod/v3/ZodError.d.cts +164 -0
  711. package/node_modules/zod/v3/ZodError.d.ts +164 -0
  712. package/node_modules/zod/v3/ZodError.js +133 -0
  713. package/node_modules/zod/v3/errors.cjs +17 -0
  714. package/node_modules/zod/v3/errors.d.cts +5 -0
  715. package/node_modules/zod/v3/errors.d.ts +5 -0
  716. package/node_modules/zod/v3/errors.js +9 -0
  717. package/node_modules/zod/v3/external.cjs +22 -0
  718. package/node_modules/zod/v3/external.d.cts +6 -0
  719. package/node_modules/zod/v3/external.d.ts +6 -0
  720. package/node_modules/zod/v3/external.js +6 -0
  721. package/node_modules/zod/v3/helpers/enumUtil.cjs +2 -0
  722. package/node_modules/zod/v3/helpers/enumUtil.d.cts +8 -0
  723. package/node_modules/zod/v3/helpers/enumUtil.d.ts +8 -0
  724. package/node_modules/zod/v3/helpers/enumUtil.js +1 -0
  725. package/node_modules/zod/v3/helpers/errorUtil.cjs +9 -0
  726. package/node_modules/zod/v3/helpers/errorUtil.d.cts +9 -0
  727. package/node_modules/zod/v3/helpers/errorUtil.d.ts +9 -0
  728. package/node_modules/zod/v3/helpers/errorUtil.js +6 -0
  729. package/node_modules/zod/v3/helpers/parseUtil.cjs +124 -0
  730. package/node_modules/zod/v3/helpers/parseUtil.d.cts +78 -0
  731. package/node_modules/zod/v3/helpers/parseUtil.d.ts +78 -0
  732. package/node_modules/zod/v3/helpers/parseUtil.js +109 -0
  733. package/node_modules/zod/v3/helpers/partialUtil.cjs +2 -0
  734. package/node_modules/zod/v3/helpers/partialUtil.d.cts +8 -0
  735. package/node_modules/zod/v3/helpers/partialUtil.d.ts +8 -0
  736. package/node_modules/zod/v3/helpers/partialUtil.js +1 -0
  737. package/node_modules/zod/v3/helpers/typeAliases.cjs +2 -0
  738. package/node_modules/zod/v3/helpers/typeAliases.d.cts +2 -0
  739. package/node_modules/zod/v3/helpers/typeAliases.d.ts +2 -0
  740. package/node_modules/zod/v3/helpers/typeAliases.js +1 -0
  741. package/node_modules/zod/v3/helpers/util.cjs +137 -0
  742. package/node_modules/zod/v3/helpers/util.d.cts +85 -0
  743. package/node_modules/zod/v3/helpers/util.d.ts +85 -0
  744. package/node_modules/zod/v3/helpers/util.js +133 -0
  745. package/node_modules/zod/v3/index.cjs +33 -0
  746. package/node_modules/zod/v3/index.d.cts +4 -0
  747. package/node_modules/zod/v3/index.d.ts +4 -0
  748. package/node_modules/zod/v3/index.js +4 -0
  749. package/node_modules/zod/v3/locales/en.cjs +111 -0
  750. package/node_modules/zod/v3/locales/en.d.cts +3 -0
  751. package/node_modules/zod/v3/locales/en.d.ts +3 -0
  752. package/node_modules/zod/v3/locales/en.js +109 -0
  753. package/node_modules/zod/v3/standard-schema.cjs +2 -0
  754. package/node_modules/zod/v3/standard-schema.d.cts +102 -0
  755. package/node_modules/zod/v3/standard-schema.d.ts +102 -0
  756. package/node_modules/zod/v3/standard-schema.js +1 -0
  757. package/node_modules/zod/v3/types.cjs +3775 -0
  758. package/node_modules/zod/v3/types.d.cts +1031 -0
  759. package/node_modules/zod/v3/types.d.ts +1031 -0
  760. package/node_modules/zod/v3/types.js +3693 -0
  761. package/node_modules/zod/v4/classic/checks.cjs +32 -0
  762. package/node_modules/zod/v4/classic/checks.d.cts +1 -0
  763. package/node_modules/zod/v4/classic/checks.d.ts +1 -0
  764. package/node_modules/zod/v4/classic/checks.js +1 -0
  765. package/node_modules/zod/v4/classic/coerce.cjs +47 -0
  766. package/node_modules/zod/v4/classic/coerce.d.cts +17 -0
  767. package/node_modules/zod/v4/classic/coerce.d.ts +17 -0
  768. package/node_modules/zod/v4/classic/coerce.js +17 -0
  769. package/node_modules/zod/v4/classic/compat.cjs +57 -0
  770. package/node_modules/zod/v4/classic/compat.d.cts +46 -0
  771. package/node_modules/zod/v4/classic/compat.d.ts +46 -0
  772. package/node_modules/zod/v4/classic/compat.js +27 -0
  773. package/node_modules/zod/v4/classic/errors.cjs +67 -0
  774. package/node_modules/zod/v4/classic/errors.d.cts +30 -0
  775. package/node_modules/zod/v4/classic/errors.d.ts +30 -0
  776. package/node_modules/zod/v4/classic/errors.js +41 -0
  777. package/node_modules/zod/v4/classic/external.cjs +70 -0
  778. package/node_modules/zod/v4/classic/external.d.cts +13 -0
  779. package/node_modules/zod/v4/classic/external.d.ts +13 -0
  780. package/node_modules/zod/v4/classic/external.js +18 -0
  781. package/node_modules/zod/v4/classic/index.cjs +33 -0
  782. package/node_modules/zod/v4/classic/index.d.cts +4 -0
  783. package/node_modules/zod/v4/classic/index.d.ts +4 -0
  784. package/node_modules/zod/v4/classic/index.js +4 -0
  785. package/node_modules/zod/v4/classic/iso.cjs +60 -0
  786. package/node_modules/zod/v4/classic/iso.d.cts +22 -0
  787. package/node_modules/zod/v4/classic/iso.d.ts +22 -0
  788. package/node_modules/zod/v4/classic/iso.js +30 -0
  789. package/node_modules/zod/v4/classic/parse.cjs +32 -0
  790. package/node_modules/zod/v4/classic/parse.d.cts +23 -0
  791. package/node_modules/zod/v4/classic/parse.d.ts +23 -0
  792. package/node_modules/zod/v4/classic/parse.js +6 -0
  793. package/node_modules/zod/v4/classic/schemas.cjs +1109 -0
  794. package/node_modules/zod/v4/classic/schemas.d.cts +630 -0
  795. package/node_modules/zod/v4/classic/schemas.d.ts +630 -0
  796. package/node_modules/zod/v4/classic/schemas.js +1006 -0
  797. package/node_modules/zod/v4/core/api.cjs +1039 -0
  798. package/node_modules/zod/v4/core/api.d.cts +284 -0
  799. package/node_modules/zod/v4/core/api.d.ts +284 -0
  800. package/node_modules/zod/v4/core/api.js +906 -0
  801. package/node_modules/zod/v4/core/checks.cjs +591 -0
  802. package/node_modules/zod/v4/core/checks.d.cts +278 -0
  803. package/node_modules/zod/v4/core/checks.d.ts +278 -0
  804. package/node_modules/zod/v4/core/checks.js +565 -0
  805. package/node_modules/zod/v4/core/core.cjs +67 -0
  806. package/node_modules/zod/v4/core/core.d.cts +49 -0
  807. package/node_modules/zod/v4/core/core.d.ts +49 -0
  808. package/node_modules/zod/v4/core/core.js +61 -0
  809. package/node_modules/zod/v4/core/doc.cjs +39 -0
  810. package/node_modules/zod/v4/core/doc.d.cts +14 -0
  811. package/node_modules/zod/v4/core/doc.d.ts +14 -0
  812. package/node_modules/zod/v4/core/doc.js +35 -0
  813. package/node_modules/zod/v4/core/errors.cjs +226 -0
  814. package/node_modules/zod/v4/core/errors.d.cts +208 -0
  815. package/node_modules/zod/v4/core/errors.d.ts +208 -0
  816. package/node_modules/zod/v4/core/errors.js +195 -0
  817. package/node_modules/zod/v4/core/function.cjs +102 -0
  818. package/node_modules/zod/v4/core/function.d.cts +52 -0
  819. package/node_modules/zod/v4/core/function.d.ts +52 -0
  820. package/node_modules/zod/v4/core/function.js +75 -0
  821. package/node_modules/zod/v4/core/index.cjs +44 -0
  822. package/node_modules/zod/v4/core/index.d.cts +15 -0
  823. package/node_modules/zod/v4/core/index.d.ts +15 -0
  824. package/node_modules/zod/v4/core/index.js +15 -0
  825. package/node_modules/zod/v4/core/json-schema.cjs +2 -0
  826. package/node_modules/zod/v4/core/json-schema.d.cts +87 -0
  827. package/node_modules/zod/v4/core/json-schema.d.ts +87 -0
  828. package/node_modules/zod/v4/core/json-schema.js +1 -0
  829. package/node_modules/zod/v4/core/parse.cjs +87 -0
  830. package/node_modules/zod/v4/core/parse.d.cts +25 -0
  831. package/node_modules/zod/v4/core/parse.d.ts +25 -0
  832. package/node_modules/zod/v4/core/parse.js +57 -0
  833. package/node_modules/zod/v4/core/regexes.cjs +103 -0
  834. package/node_modules/zod/v4/core/regexes.d.cts +62 -0
  835. package/node_modules/zod/v4/core/regexes.d.ts +62 -0
  836. package/node_modules/zod/v4/core/regexes.js +95 -0
  837. package/node_modules/zod/v4/core/registries.cjs +56 -0
  838. package/node_modules/zod/v4/core/registries.d.cts +35 -0
  839. package/node_modules/zod/v4/core/registries.d.ts +35 -0
  840. package/node_modules/zod/v4/core/registries.js +51 -0
  841. package/node_modules/zod/v4/core/schemas.cjs +1748 -0
  842. package/node_modules/zod/v4/core/schemas.d.cts +1041 -0
  843. package/node_modules/zod/v4/core/schemas.d.ts +1041 -0
  844. package/node_modules/zod/v4/core/schemas.js +1717 -0
  845. package/node_modules/zod/v4/core/standard-schema.cjs +2 -0
  846. package/node_modules/zod/v4/core/standard-schema.d.cts +55 -0
  847. package/node_modules/zod/v4/core/standard-schema.d.ts +55 -0
  848. package/node_modules/zod/v4/core/standard-schema.js +1 -0
  849. package/node_modules/zod/v4/core/to-json-schema.cjs +854 -0
  850. package/node_modules/zod/v4/core/to-json-schema.d.cts +88 -0
  851. package/node_modules/zod/v4/core/to-json-schema.d.ts +88 -0
  852. package/node_modules/zod/v4/core/to-json-schema.js +849 -0
  853. package/node_modules/zod/v4/core/util.cjs +539 -0
  854. package/node_modules/zod/v4/core/util.d.cts +183 -0
  855. package/node_modules/zod/v4/core/util.d.ts +183 -0
  856. package/node_modules/zod/v4/core/util.js +493 -0
  857. package/node_modules/zod/v4/core/versions.cjs +8 -0
  858. package/node_modules/zod/v4/core/versions.d.cts +5 -0
  859. package/node_modules/zod/v4/core/versions.d.ts +5 -0
  860. package/node_modules/zod/v4/core/versions.js +5 -0
  861. package/node_modules/zod/v4/index.cjs +22 -0
  862. package/node_modules/zod/v4/index.d.cts +3 -0
  863. package/node_modules/zod/v4/index.d.ts +3 -0
  864. package/node_modules/zod/v4/index.js +3 -0
  865. package/node_modules/zod/v4/locales/ar.cjs +142 -0
  866. package/node_modules/zod/v4/locales/ar.d.cts +4 -0
  867. package/node_modules/zod/v4/locales/ar.d.ts +4 -0
  868. package/node_modules/zod/v4/locales/ar.js +116 -0
  869. package/node_modules/zod/v4/locales/az.cjs +141 -0
  870. package/node_modules/zod/v4/locales/az.d.cts +4 -0
  871. package/node_modules/zod/v4/locales/az.d.ts +4 -0
  872. package/node_modules/zod/v4/locales/az.js +115 -0
  873. package/node_modules/zod/v4/locales/be.cjs +190 -0
  874. package/node_modules/zod/v4/locales/be.d.cts +4 -0
  875. package/node_modules/zod/v4/locales/be.d.ts +4 -0
  876. package/node_modules/zod/v4/locales/be.js +164 -0
  877. package/node_modules/zod/v4/locales/ca.cjs +144 -0
  878. package/node_modules/zod/v4/locales/ca.d.cts +4 -0
  879. package/node_modules/zod/v4/locales/ca.d.ts +4 -0
  880. package/node_modules/zod/v4/locales/ca.js +118 -0
  881. package/node_modules/zod/v4/locales/cs.cjs +161 -0
  882. package/node_modules/zod/v4/locales/cs.d.cts +4 -0
  883. package/node_modules/zod/v4/locales/cs.d.ts +4 -0
  884. package/node_modules/zod/v4/locales/cs.js +135 -0
  885. package/node_modules/zod/v4/locales/de.cjs +142 -0
  886. package/node_modules/zod/v4/locales/de.d.cts +4 -0
  887. package/node_modules/zod/v4/locales/de.d.ts +4 -0
  888. package/node_modules/zod/v4/locales/de.js +116 -0
  889. package/node_modules/zod/v4/locales/en.cjs +145 -0
  890. package/node_modules/zod/v4/locales/en.d.cts +5 -0
  891. package/node_modules/zod/v4/locales/en.d.ts +5 -0
  892. package/node_modules/zod/v4/locales/en.js +117 -0
  893. package/node_modules/zod/v4/locales/eo.cjs +144 -0
  894. package/node_modules/zod/v4/locales/eo.d.cts +5 -0
  895. package/node_modules/zod/v4/locales/eo.d.ts +5 -0
  896. package/node_modules/zod/v4/locales/eo.js +116 -0
  897. package/node_modules/zod/v4/locales/es.cjs +143 -0
  898. package/node_modules/zod/v4/locales/es.d.cts +4 -0
  899. package/node_modules/zod/v4/locales/es.d.ts +4 -0
  900. package/node_modules/zod/v4/locales/es.js +117 -0
  901. package/node_modules/zod/v4/locales/fa.cjs +148 -0
  902. package/node_modules/zod/v4/locales/fa.d.cts +4 -0
  903. package/node_modules/zod/v4/locales/fa.d.ts +4 -0
  904. package/node_modules/zod/v4/locales/fa.js +122 -0
  905. package/node_modules/zod/v4/locales/fi.cjs +148 -0
  906. package/node_modules/zod/v4/locales/fi.d.cts +4 -0
  907. package/node_modules/zod/v4/locales/fi.d.ts +4 -0
  908. package/node_modules/zod/v4/locales/fi.js +122 -0
  909. package/node_modules/zod/v4/locales/fr-CA.cjs +143 -0
  910. package/node_modules/zod/v4/locales/fr-CA.d.cts +4 -0
  911. package/node_modules/zod/v4/locales/fr-CA.d.ts +4 -0
  912. package/node_modules/zod/v4/locales/fr-CA.js +117 -0
  913. package/node_modules/zod/v4/locales/fr.cjs +142 -0
  914. package/node_modules/zod/v4/locales/fr.d.cts +4 -0
  915. package/node_modules/zod/v4/locales/fr.d.ts +4 -0
  916. package/node_modules/zod/v4/locales/fr.js +116 -0
  917. package/node_modules/zod/v4/locales/he.cjs +143 -0
  918. package/node_modules/zod/v4/locales/he.d.cts +4 -0
  919. package/node_modules/zod/v4/locales/he.d.ts +4 -0
  920. package/node_modules/zod/v4/locales/he.js +117 -0
  921. package/node_modules/zod/v4/locales/hu.cjs +143 -0
  922. package/node_modules/zod/v4/locales/hu.d.cts +4 -0
  923. package/node_modules/zod/v4/locales/hu.d.ts +4 -0
  924. package/node_modules/zod/v4/locales/hu.js +117 -0
  925. package/node_modules/zod/v4/locales/id.cjs +142 -0
  926. package/node_modules/zod/v4/locales/id.d.cts +4 -0
  927. package/node_modules/zod/v4/locales/id.d.ts +4 -0
  928. package/node_modules/zod/v4/locales/id.js +116 -0
  929. package/node_modules/zod/v4/locales/index.cjs +84 -0
  930. package/node_modules/zod/v4/locales/index.d.cts +39 -0
  931. package/node_modules/zod/v4/locales/index.d.ts +39 -0
  932. package/node_modules/zod/v4/locales/index.js +39 -0
  933. package/node_modules/zod/v4/locales/it.cjs +143 -0
  934. package/node_modules/zod/v4/locales/it.d.cts +4 -0
  935. package/node_modules/zod/v4/locales/it.d.ts +4 -0
  936. package/node_modules/zod/v4/locales/it.js +117 -0
  937. package/node_modules/zod/v4/locales/ja.cjs +141 -0
  938. package/node_modules/zod/v4/locales/ja.d.cts +4 -0
  939. package/node_modules/zod/v4/locales/ja.d.ts +4 -0
  940. package/node_modules/zod/v4/locales/ja.js +115 -0
  941. package/node_modules/zod/v4/locales/kh.cjs +143 -0
  942. package/node_modules/zod/v4/locales/kh.d.cts +4 -0
  943. package/node_modules/zod/v4/locales/kh.d.ts +4 -0
  944. package/node_modules/zod/v4/locales/kh.js +117 -0
  945. package/node_modules/zod/v4/locales/ko.cjs +147 -0
  946. package/node_modules/zod/v4/locales/ko.d.cts +4 -0
  947. package/node_modules/zod/v4/locales/ko.d.ts +4 -0
  948. package/node_modules/zod/v4/locales/ko.js +121 -0
  949. package/node_modules/zod/v4/locales/mk.cjs +144 -0
  950. package/node_modules/zod/v4/locales/mk.d.cts +4 -0
  951. package/node_modules/zod/v4/locales/mk.d.ts +4 -0
  952. package/node_modules/zod/v4/locales/mk.js +118 -0
  953. package/node_modules/zod/v4/locales/ms.cjs +142 -0
  954. package/node_modules/zod/v4/locales/ms.d.cts +4 -0
  955. package/node_modules/zod/v4/locales/ms.d.ts +4 -0
  956. package/node_modules/zod/v4/locales/ms.js +116 -0
  957. package/node_modules/zod/v4/locales/nl.cjs +143 -0
  958. package/node_modules/zod/v4/locales/nl.d.cts +4 -0
  959. package/node_modules/zod/v4/locales/nl.d.ts +4 -0
  960. package/node_modules/zod/v4/locales/nl.js +117 -0
  961. package/node_modules/zod/v4/locales/no.cjs +142 -0
  962. package/node_modules/zod/v4/locales/no.d.cts +4 -0
  963. package/node_modules/zod/v4/locales/no.d.ts +4 -0
  964. package/node_modules/zod/v4/locales/no.js +116 -0
  965. package/node_modules/zod/v4/locales/ota.cjs +143 -0
  966. package/node_modules/zod/v4/locales/ota.d.cts +4 -0
  967. package/node_modules/zod/v4/locales/ota.d.ts +4 -0
  968. package/node_modules/zod/v4/locales/ota.js +117 -0
  969. package/node_modules/zod/v4/locales/pl.cjs +143 -0
  970. package/node_modules/zod/v4/locales/pl.d.cts +4 -0
  971. package/node_modules/zod/v4/locales/pl.d.ts +4 -0
  972. package/node_modules/zod/v4/locales/pl.js +117 -0
  973. package/node_modules/zod/v4/locales/ps.cjs +148 -0
  974. package/node_modules/zod/v4/locales/ps.d.cts +4 -0
  975. package/node_modules/zod/v4/locales/ps.d.ts +4 -0
  976. package/node_modules/zod/v4/locales/ps.js +122 -0
  977. package/node_modules/zod/v4/locales/pt.cjs +142 -0
  978. package/node_modules/zod/v4/locales/pt.d.cts +4 -0
  979. package/node_modules/zod/v4/locales/pt.d.ts +4 -0
  980. package/node_modules/zod/v4/locales/pt.js +116 -0
  981. package/node_modules/zod/v4/locales/ru.cjs +190 -0
  982. package/node_modules/zod/v4/locales/ru.d.cts +4 -0
  983. package/node_modules/zod/v4/locales/ru.d.ts +4 -0
  984. package/node_modules/zod/v4/locales/ru.js +164 -0
  985. package/node_modules/zod/v4/locales/sl.cjs +143 -0
  986. package/node_modules/zod/v4/locales/sl.d.cts +4 -0
  987. package/node_modules/zod/v4/locales/sl.d.ts +4 -0
  988. package/node_modules/zod/v4/locales/sl.js +117 -0
  989. package/node_modules/zod/v4/locales/sv.cjs +144 -0
  990. package/node_modules/zod/v4/locales/sv.d.cts +4 -0
  991. package/node_modules/zod/v4/locales/sv.d.ts +4 -0
  992. package/node_modules/zod/v4/locales/sv.js +118 -0
  993. package/node_modules/zod/v4/locales/ta.cjs +143 -0
  994. package/node_modules/zod/v4/locales/ta.d.cts +4 -0
  995. package/node_modules/zod/v4/locales/ta.d.ts +4 -0
  996. package/node_modules/zod/v4/locales/ta.js +117 -0
  997. package/node_modules/zod/v4/locales/th.cjs +143 -0
  998. package/node_modules/zod/v4/locales/th.d.cts +4 -0
  999. package/node_modules/zod/v4/locales/th.d.ts +4 -0
  1000. package/node_modules/zod/v4/locales/th.js +117 -0
  1001. package/node_modules/zod/v4/locales/tr.cjs +143 -0
  1002. package/node_modules/zod/v4/locales/tr.d.cts +5 -0
  1003. package/node_modules/zod/v4/locales/tr.d.ts +5 -0
  1004. package/node_modules/zod/v4/locales/tr.js +115 -0
  1005. package/node_modules/zod/v4/locales/ua.cjs +143 -0
  1006. package/node_modules/zod/v4/locales/ua.d.cts +4 -0
  1007. package/node_modules/zod/v4/locales/ua.d.ts +4 -0
  1008. package/node_modules/zod/v4/locales/ua.js +117 -0
  1009. package/node_modules/zod/v4/locales/ur.cjs +143 -0
  1010. package/node_modules/zod/v4/locales/ur.d.cts +4 -0
  1011. package/node_modules/zod/v4/locales/ur.d.ts +4 -0
  1012. package/node_modules/zod/v4/locales/ur.js +117 -0
  1013. package/node_modules/zod/v4/locales/vi.cjs +142 -0
  1014. package/node_modules/zod/v4/locales/vi.d.cts +4 -0
  1015. package/node_modules/zod/v4/locales/vi.d.ts +4 -0
  1016. package/node_modules/zod/v4/locales/vi.js +116 -0
  1017. package/node_modules/zod/v4/locales/zh-CN.cjs +142 -0
  1018. package/node_modules/zod/v4/locales/zh-CN.d.cts +4 -0
  1019. package/node_modules/zod/v4/locales/zh-CN.d.ts +4 -0
  1020. package/node_modules/zod/v4/locales/zh-CN.js +116 -0
  1021. package/node_modules/zod/v4/locales/zh-TW.cjs +143 -0
  1022. package/node_modules/zod/v4/locales/zh-TW.d.cts +4 -0
  1023. package/node_modules/zod/v4/locales/zh-TW.d.ts +4 -0
  1024. package/node_modules/zod/v4/locales/zh-TW.js +117 -0
  1025. package/node_modules/zod/v4/mini/checks.cjs +34 -0
  1026. package/node_modules/zod/v4/mini/checks.d.cts +1 -0
  1027. package/node_modules/zod/v4/mini/checks.d.ts +1 -0
  1028. package/node_modules/zod/v4/mini/checks.js +1 -0
  1029. package/node_modules/zod/v4/mini/coerce.cjs +47 -0
  1030. package/node_modules/zod/v4/mini/coerce.d.cts +7 -0
  1031. package/node_modules/zod/v4/mini/coerce.d.ts +7 -0
  1032. package/node_modules/zod/v4/mini/coerce.js +17 -0
  1033. package/node_modules/zod/v4/mini/external.cjs +62 -0
  1034. package/node_modules/zod/v4/mini/external.d.cts +11 -0
  1035. package/node_modules/zod/v4/mini/external.d.ts +11 -0
  1036. package/node_modules/zod/v4/mini/external.js +13 -0
  1037. package/node_modules/zod/v4/mini/index.cjs +32 -0
  1038. package/node_modules/zod/v4/mini/index.d.cts +3 -0
  1039. package/node_modules/zod/v4/mini/index.d.ts +3 -0
  1040. package/node_modules/zod/v4/mini/index.js +3 -0
  1041. package/node_modules/zod/v4/mini/iso.cjs +60 -0
  1042. package/node_modules/zod/v4/mini/iso.d.cts +22 -0
  1043. package/node_modules/zod/v4/mini/iso.d.ts +22 -0
  1044. package/node_modules/zod/v4/mini/iso.js +30 -0
  1045. package/node_modules/zod/v4/mini/parse.cjs +8 -0
  1046. package/node_modules/zod/v4/mini/parse.d.cts +1 -0
  1047. package/node_modules/zod/v4/mini/parse.d.ts +1 -0
  1048. package/node_modules/zod/v4/mini/parse.js +1 -0
  1049. package/node_modules/zod/v4/mini/schemas.cjs +839 -0
  1050. package/node_modules/zod/v4/mini/schemas.d.cts +356 -0
  1051. package/node_modules/zod/v4/mini/schemas.d.ts +356 -0
  1052. package/node_modules/zod/v4/mini/schemas.js +732 -0
  1053. package/node_modules/zod/v4-mini/index.cjs +17 -0
  1054. package/node_modules/zod/v4-mini/index.d.cts +1 -0
  1055. package/node_modules/zod/v4-mini/index.d.ts +1 -0
  1056. package/node_modules/zod/v4-mini/index.js +1 -0
  1057. package/package.json +97 -0
@@ -0,0 +1,2605 @@
1
+ import { quote } from '../utils/shell-quote.js';
2
+ import { logForDebugging } from '../utils/debug.js';
3
+ import { whichSync } from '../utils/which.js';
4
+ import { randomBytes } from 'node:crypto';
5
+ import * as fs from 'fs';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { endianness, tmpdir } from 'node:os';
8
+ import path, { join } from 'node:path';
9
+ import { ripGrep } from '../utils/ripgrep.js';
10
+ import { buildJavaToolOptions } from './java-proxy-agent.js';
11
+ import { generateProxyEnvVars, buildPosixGitSafeDirEnv, normalizePathForSandbox, normalizeCaseForComparison, isSymlinkOutsideBoundary, encodeSandboxedCommand, attributionKeyFor, DANGEROUS_FILES, isAbsenceErrno, isAtOrUnder, isStrictlyUnder, getDangerousDirectories, } from './sandbox-utils.js';
12
+ import { getApplySeccompBinaryPath } from './generate-seccomp-filter.js';
13
+ /** Default max depth for searching dangerous files */
14
+ const DEFAULT_MANDATORY_DENY_SEARCH_DEPTH = 3;
15
+ /**
16
+ * Find if any component of the path is a symlink within the allowed write paths.
17
+ * Returns the symlink path if found, or null if no symlinks.
18
+ *
19
+ * This is used to detect and block symlink replacement attacks where an attacker
20
+ * could delete a symlink and create a real directory with malicious content.
21
+ */
22
+ function findSymlinkInPath(targetPath, allowedWritePaths) {
23
+ const parts = targetPath.split(path.sep);
24
+ let currentPath = '';
25
+ for (const part of parts) {
26
+ if (!part)
27
+ continue; // Skip empty parts (leading /)
28
+ const nextPath = currentPath + path.sep + part;
29
+ try {
30
+ const stats = fs.lstatSync(nextPath);
31
+ if (stats.isSymbolicLink()) {
32
+ // Check if this symlink is within an allowed write path
33
+ const isWithinAllowedPath = allowedWritePaths.some(allowedPath => isAtOrUnder(nextPath, allowedPath));
34
+ if (isWithinAllowedPath) {
35
+ return nextPath;
36
+ }
37
+ }
38
+ }
39
+ catch {
40
+ // Path doesn't exist - no symlink issue here
41
+ break;
42
+ }
43
+ currentPath = nextPath;
44
+ }
45
+ return null;
46
+ }
47
+ /** Bounded depth for chasing dangling symlink chains (kernel ELOOP limit). */
48
+ const MAX_SYMLINK_RESOLUTION_DEPTH = 40;
49
+ /**
50
+ * Canonicalize a deny path through symlinks before any mask or bind is
51
+ * computed (resolve-before-mask). Deny paths regularly contain symlinked
52
+ * ancestors — the common dotfiles setup symlinks .claude (or the parent of
53
+ * .mcp.json) to a real directory — and masking the raw symlink with
54
+ * `--ro-bind /dev/null <symlink>` makes bwrap abort at startup: "Is a
55
+ * directory" for relative link targets, ENOENT for absolute ones.
56
+ *
57
+ * Resolution: realpath when the full path exists; otherwise canonicalize the
58
+ * deepest existing ancestor and re-join the missing components. A dangling
59
+ * symlink along the way is chain-walked (bounded) so the deny lands where a
60
+ * write through the link would actually create the file. Returns null when
61
+ * the path cannot be canonicalized (e.g. a symlink cycle); callers should
62
+ * skip such paths rather than emit a mask bwrap will reject.
63
+ *
64
+ * Unlike normalizePathForSandbox, this intentionally applies no
65
+ * isSymlinkOutsideBoundary check: that check exists so allow paths can't
66
+ * silently widen write access through a symlink, but a deny path only ever
67
+ * removes access, and the mask must land on the inode that writes through
68
+ * the symlink actually reach.
69
+ */
70
+ function resolveSymlinkedDenyPath(targetPath) {
71
+ let current = targetPath;
72
+ for (let i = 0; i < MAX_SYMLINK_RESOLUTION_DEPTH; i++) {
73
+ try {
74
+ return fs.realpathSync(current);
75
+ }
76
+ catch {
77
+ // Some component is missing or dangling — canonicalize manually below.
78
+ }
79
+ // Find the deepest ancestor that fully resolves, collecting the missing
80
+ // suffix components (leaf first, so unshift keeps them in path order).
81
+ let ancestor = current;
82
+ const remainder = [];
83
+ let resolvedAncestor = null;
84
+ while (resolvedAncestor === null) {
85
+ const parent = path.dirname(ancestor);
86
+ if (parent === ancestor) {
87
+ return null; // reached the root without resolving anything
88
+ }
89
+ remainder.unshift(path.basename(ancestor));
90
+ ancestor = parent;
91
+ try {
92
+ resolvedAncestor = fs.realpathSync(ancestor);
93
+ }
94
+ catch {
95
+ // Keep walking up.
96
+ }
97
+ }
98
+ // The first missing component may be a dangling symlink (lstat succeeds,
99
+ // realpath fails). Follow it and loop to re-canonicalize; otherwise the
100
+ // remainder is genuinely non-existent and the re-joined path is final.
101
+ const firstMissing = path.join(resolvedAncestor, remainder[0]);
102
+ let linkTarget = null;
103
+ try {
104
+ linkTarget = fs.readlinkSync(firstMissing);
105
+ }
106
+ catch {
107
+ // Not a symlink — nothing left to resolve.
108
+ }
109
+ if (linkTarget === null) {
110
+ return path.join(resolvedAncestor, ...remainder);
111
+ }
112
+ current = path.join(path.resolve(path.dirname(firstMissing), linkTarget), ...remainder.slice(1));
113
+ }
114
+ return null; // symlink chain too long or cyclic
115
+ }
116
+ /**
117
+ * Check if any existing component in the path is a file (not a directory).
118
+ * If so, the target path can never be created because you can't mkdir under a file.
119
+ *
120
+ * This handles the git worktree case: .git is a file, so .git/hooks can never
121
+ * exist and there's nothing to deny.
122
+ */
123
+ function hasFileAncestor(targetPath) {
124
+ const parts = targetPath.split(path.sep);
125
+ let currentPath = '';
126
+ for (const part of parts) {
127
+ if (!part)
128
+ continue; // Skip empty parts (leading /)
129
+ const nextPath = currentPath + path.sep + part;
130
+ try {
131
+ const stat = fs.statSync(nextPath);
132
+ if (stat.isFile()) {
133
+ // This component exists as a file — nothing below it can be created
134
+ return true;
135
+ }
136
+ }
137
+ catch {
138
+ // Path doesn't exist — stop checking
139
+ break;
140
+ }
141
+ currentPath = nextPath;
142
+ }
143
+ return false;
144
+ }
145
+ /**
146
+ * Find the first non-existent path component.
147
+ * E.g., for "/existing/parent/nonexistent/child/file.txt" where /existing/parent exists,
148
+ * returns "/existing/parent/nonexistent"
149
+ *
150
+ * This is used to block creation of non-existent deny paths by mounting /dev/null
151
+ * at the first missing component, preventing mkdir from creating the parent directories.
152
+ */
153
+ function findFirstNonExistentComponent(targetPath) {
154
+ const parts = targetPath.split(path.sep);
155
+ let currentPath = '';
156
+ for (const part of parts) {
157
+ if (!part)
158
+ continue; // Skip empty parts (leading /)
159
+ const nextPath = currentPath + path.sep + part;
160
+ if (!fs.existsSync(nextPath)) {
161
+ return nextPath;
162
+ }
163
+ currentPath = nextPath;
164
+ }
165
+ return targetPath; // Shouldn't reach here if called correctly
166
+ }
167
+ /**
168
+ * The part of the mandatory deny set that follows from the cwd alone: the
169
+ * dangerous files and directories resolved against it, plus `.git/hooks` and
170
+ * (unless the caller allows git config) `.git/config`.
171
+ * {@link linuxGetMandatoryDenyPaths} adds the nested matches its ripgrep scan
172
+ * finds on top of these. Split out so a consumer that must not scan — the
173
+ * violation monitor, which needs the same denies to judge a write bwrap
174
+ * refuses — reads the same definition rather than a copy of it.
175
+ */
176
+ export function linuxGetCwdMandatoryDenyPaths(allowGitConfig = false) {
177
+ const cwd = process.cwd();
178
+ // Note: Settings files are added at the callsite in sandbox-manager.ts
179
+ const denyPaths = [
180
+ // Dangerous files in CWD
181
+ ...DANGEROUS_FILES.map(f => path.resolve(cwd, f)),
182
+ // Dangerous directories in CWD
183
+ ...getDangerousDirectories().map(d => path.resolve(cwd, d)),
184
+ ];
185
+ // Git hooks and config are only denied when .git exists as a directory.
186
+ // In git worktrees, .git is a file (e.g., "gitdir: /path/..."), so
187
+ // .git/hooks can never exist — denying it would cause bwrap to fail.
188
+ // When .git doesn't exist at all, mounting at .git would block its
189
+ // creation and break git init.
190
+ const dotGitPath = path.resolve(cwd, '.git');
191
+ let dotGitIsDirectory = false;
192
+ try {
193
+ dotGitIsDirectory = fs.statSync(dotGitPath).isDirectory();
194
+ }
195
+ catch {
196
+ // .git doesn't exist
197
+ }
198
+ if (dotGitIsDirectory) {
199
+ // Git hooks always blocked for security
200
+ denyPaths.push(path.resolve(cwd, '.git/hooks'));
201
+ // Git config conditionally blocked based on allowGitConfig setting
202
+ if (!allowGitConfig) {
203
+ denyPaths.push(path.resolve(cwd, '.git/config'));
204
+ }
205
+ }
206
+ return denyPaths;
207
+ }
208
+ /**
209
+ * Get mandatory deny paths using ripgrep (Linux only).
210
+ * Uses a SINGLE ripgrep call with multiple glob patterns for efficiency.
211
+ * With --max-depth limiting, this is fast enough to run on each command without memoization.
212
+ */
213
+ async function linuxGetMandatoryDenyPaths(ripgrepConfig = { command: 'rg' }, maxDepth = DEFAULT_MANDATORY_DENY_SEARCH_DEPTH, allowGitConfig = false, abortSignal) {
214
+ const cwd = process.cwd();
215
+ // Use provided signal or create a fallback controller
216
+ const fallbackController = new AbortController();
217
+ const signal = abortSignal ?? fallbackController.signal;
218
+ const dangerousDirectories = getDangerousDirectories();
219
+ const denyPaths = linuxGetCwdMandatoryDenyPaths(allowGitConfig);
220
+ // Build iglob args for all patterns in one ripgrep call
221
+ const iglobArgs = [];
222
+ for (const fileName of DANGEROUS_FILES) {
223
+ iglobArgs.push('--iglob', fileName);
224
+ }
225
+ for (const dirName of dangerousDirectories) {
226
+ iglobArgs.push('--iglob', `**/${dirName}/**`);
227
+ }
228
+ // Git hooks always blocked in nested repos
229
+ iglobArgs.push('--iglob', '**/.git/hooks/**');
230
+ // Git config conditionally blocked in nested repos
231
+ if (!allowGitConfig) {
232
+ iglobArgs.push('--iglob', '**/.git/config');
233
+ }
234
+ // Single ripgrep call to find all dangerous paths in subdirectories
235
+ // Limit depth for performance - deeply nested dangerous files are rare
236
+ // and the security benefit doesn't justify the traversal cost
237
+ let matches = [];
238
+ try {
239
+ matches = await ripGrep([
240
+ '--files',
241
+ '--hidden',
242
+ '--max-depth',
243
+ String(maxDepth),
244
+ ...iglobArgs,
245
+ '-g',
246
+ '!**/node_modules/**',
247
+ ], cwd, signal, ripgrepConfig);
248
+ }
249
+ catch (error) {
250
+ logForDebugging(`[Sandbox] ripgrep scan failed: ${error}`);
251
+ }
252
+ // Process matches
253
+ for (const match of matches) {
254
+ const absolutePath = path.resolve(cwd, match);
255
+ // File inside a dangerous directory -> add the directory path
256
+ let foundDir = false;
257
+ for (const dirName of [...dangerousDirectories, '.git']) {
258
+ const normalizedDirName = normalizeCaseForComparison(dirName);
259
+ const segments = absolutePath.split(path.sep);
260
+ const dirIndex = segments.findIndex(s => normalizeCaseForComparison(s) === normalizedDirName);
261
+ if (dirIndex !== -1) {
262
+ // For .git, we want hooks/ or config, not the whole .git dir
263
+ if (dirName === '.git') {
264
+ const gitDir = segments.slice(0, dirIndex + 1).join(path.sep);
265
+ if (match.includes('.git/hooks')) {
266
+ denyPaths.push(path.join(gitDir, 'hooks'));
267
+ }
268
+ else if (match.includes('.git/config')) {
269
+ denyPaths.push(path.join(gitDir, 'config'));
270
+ }
271
+ }
272
+ else {
273
+ denyPaths.push(segments.slice(0, dirIndex + 1).join(path.sep));
274
+ }
275
+ foundDir = true;
276
+ break;
277
+ }
278
+ }
279
+ // Dangerous file match
280
+ if (!foundDir) {
281
+ denyPaths.push(absolutePath);
282
+ }
283
+ }
284
+ return [...new Set(denyPaths)];
285
+ }
286
+ // Track mount points created by bwrap for non-existent deny paths.
287
+ // When bwrap does --ro-bind /dev/null /nonexistent/path, it creates an empty
288
+ // file on the host as a mount point. These persist after bwrap exits and must
289
+ // be cleaned up explicitly.
290
+ const bwrapMountPoints = new Set();
291
+ // The source of the empty-directory mount points: at most one at a time, made
292
+ // on first use, reused while a sandbox is running, and removed with the mount
293
+ // points — never before, because a live bind's source must stay.
294
+ // generateFilesystemArgs pins it read-only inside every sandbox that uses it.
295
+ let emptyMountSourceDir;
296
+ function ensureEmptyMountSourceDir() {
297
+ if (emptyMountSourceDir !== undefined) {
298
+ // Revalidated, not trusted: the path is under the system temp dir, which
299
+ // sandboxed commands commonly can write, and bwrap resolves a bind source
300
+ // on the HOST, so a directory swapped for a symlink between two wraps
301
+ // would publish whatever it points at. Anything that is not our own
302
+ // private empty directory is left where it is: it is not ours to delete.
303
+ try {
304
+ const stat = fs.lstatSync(emptyMountSourceDir);
305
+ if (stat.isDirectory() &&
306
+ stat.uid === process.getuid?.() &&
307
+ (stat.mode & 0o077) === 0 &&
308
+ fs.readdirSync(emptyMountSourceDir).length === 0) {
309
+ return emptyMountSourceDir;
310
+ }
311
+ }
312
+ catch {
313
+ // Gone, or no longer inspectable: make a new one below.
314
+ }
315
+ logForDebugging(`[Sandbox Linux] Not reusing the empty-directory mount source, it is gone or no longer our own empty directory: ${emptyMountSourceDir}`);
316
+ }
317
+ emptyMountSourceDir = fs.mkdtempSync(path.join(tmpdir(), 'claude-empty-'));
318
+ // mkdtemp asks for 0700 but the umask applies, so under `umask 0200` the
319
+ // directory lands on 0500. Set the mode here rather than let the check
320
+ // above reject the directory this call has just made.
321
+ fs.chmodSync(emptyMountSourceDir, 0o700);
322
+ return emptyMountSourceDir;
323
+ }
324
+ /**
325
+ * Is `p` a mount point an earlier sandbox left behind? bwrap makes the mount
326
+ * point for `--ro-bind /dev/null <absent path>` with ensure_file(dest, 0444):
327
+ * an empty regular file with no write bits. The set above lives in memory, so
328
+ * a process that dies without an 'exit' event (SIGKILL, OOM) leaves the file
329
+ * on the host with nothing left that knows what it is. Files that only look
330
+ * similar are written by their creators with write bits (a lockfile, an empty
331
+ * file materialised on purpose: 0666 & ~umask) or have content or a second
332
+ * link. An empty read-only DIRECTORY left the same way is not recognisable:
333
+ * it looks like anyone's empty directory.
334
+ */
335
+ function isStaleBwrapMountPoint(p) {
336
+ try {
337
+ const stat = fs.lstatSync(p);
338
+ return (stat.isFile() &&
339
+ stat.size === 0 &&
340
+ (stat.mode & 0o222) === 0 &&
341
+ stat.nlink === 1);
342
+ }
343
+ catch {
344
+ return false;
345
+ }
346
+ }
347
+ export const CAP_SETFCAP = 31;
348
+ // This process's bounding set unioned with its inheritable set, read once —
349
+ // see processHasBoundingCapability for why those two. Only prctl(CAPBSET_DROP)
350
+ // and capset move them and this library calls neither, and without the memo
351
+ // the read is a synchronous open of /proc/self/status on the path of every
352
+ // wrapped command. Only a successful read is stored, so a transient failure
353
+ // does not latch "no capabilities" for the life of the process.
354
+ let boundingCapabilities;
355
+ let capabilityReadFailureLogged = false;
356
+ /**
357
+ * Whether this process holds `cap` in its bounding or inheritable set (Linux).
358
+ *
359
+ * bwrap is reached by execve, and for a euid-0 caller the kernel recomputes
360
+ * the new permitted set from the bounding and inheritable sets, discarding
361
+ * what the caller itself had permitted. Those two sets are therefore what
362
+ * decides which capabilities bwrap holds when it writes its uid map; CapPrm
363
+ * and CapEff do not, and for any process itself reached by exec as root they
364
+ * merely repeat the union.
365
+ */
366
+ export function processHasBoundingCapability(cap) {
367
+ let bits = boundingCapabilities;
368
+ if (bits === undefined) {
369
+ let failure = 'has no CapBnd or CapInh line';
370
+ try {
371
+ bits = boundingCapabilitiesFromStatus(fs.readFileSync('/proc/self/status', 'utf8'));
372
+ boundingCapabilities = bits;
373
+ }
374
+ catch (e) {
375
+ failure = `could not be read (${String(e)})`;
376
+ }
377
+ if (bits === undefined) {
378
+ if (!capabilityReadFailureLogged) {
379
+ capabilityReadFailureLogged = true;
380
+ logForDebugging(`[Sandbox Linux] /proc/self/status ${failure} - assuming this process holds no capabilities`, { level: 'warn' });
381
+ }
382
+ return false;
383
+ }
384
+ }
385
+ return ((bits >> BigInt(cap)) & 1n) === 1n;
386
+ }
387
+ /**
388
+ * The capability set an execve gives a euid-0 caller's child, parsed out of a
389
+ * `/proc/<pid>/status`: CapBnd unioned with CapInh. `undefined` when either
390
+ * line is absent. CapPrm and CapEff are deliberately not consulted — see
391
+ * processHasBoundingCapability.
392
+ */
393
+ export function boundingCapabilitiesFromStatus(status) {
394
+ const bounding = status.match(/^CapBnd:\s*([0-9a-fA-F]+)\s*$/m);
395
+ const inheritable = status.match(/^CapInh:\s*([0-9a-fA-F]+)\s*$/m);
396
+ return bounding && inheritable
397
+ ? BigInt('0x' + bounding[1]) | BigInt('0x' + inheritable[1])
398
+ : undefined;
399
+ }
400
+ /**
401
+ * The bwrap capability list. Always `--cap-drop ALL`, which for a non-root
402
+ * caller is bwrap's default anyway. `--cap-add CAP_SETFCAP` for the whole
403
+ * bwrap invocation — the outer shell and the two socat relays hold it too —
404
+ * when the caller is uid 0, the seccomp helper is in use and the capability
405
+ * survives into bwrap: the helper's nested user namespace maps uid 0, which
406
+ * Linux 5.12 and the distribution kernels that backported it allow only from
407
+ * a creator holding CAP_SETFCAP. Under the helper a uid-0 caller's command
408
+ * therefore has a full set inside that nested namespace, which is
409
+ * identity-mapped to the caller's uid 0, and the filesystem policy there
410
+ * rests on the nested namespace's mount copies being locked. Without the
411
+ * helper (`allowAllUnixSockets`, or no usable helper binary) the command runs
412
+ * in bwrap's own namespaces and `--cap-drop ALL` is what stops it unmounting
413
+ * a deny.
414
+ *
415
+ * Pure: `hasSetfcap` is decided by the caller, which is also where the
416
+ * missing-capability case is reported once per process.
417
+ */
418
+ export function capabilityArgs({ euid, hasSetfcap, usesSeccompHelper, }) {
419
+ const args = ['--cap-drop', 'ALL'];
420
+ if (euid !== 0)
421
+ return args;
422
+ if (!hasSetfcap)
423
+ return args;
424
+ if (usesSeccompHelper)
425
+ args.push('--cap-add', 'CAP_SETFCAP');
426
+ return args;
427
+ }
428
+ // Latch for the predicted case: once per process, not once per command.
429
+ let setfcapMissingLogged = false;
430
+ // A uid-0 caller whose bounding set lacks CAP_SETFCAP cannot sandbox
431
+ // anything: bwrap's own --unshare-user writes a uid map containing uid 0, and
432
+ // so does the seccomp helper's nested namespace, and a kernel that enforces
433
+ // the requirement refuses both unless the namespace's creator held the
434
+ // capability. allowAllUnixSockets does not avoid it — it only drops the
435
+ // helper, not bwrap's own user namespace. Worded for both the predicted case
436
+ // (the debug warning on the path of a wrapped command) and the confirmed one
437
+ // (checkLinuxDependencies, after bubblewrap has actually refused).
438
+ export const CAP_SETFCAP_MISSING_MESSAGE = "running as uid 0 without CAP_SETFCAP in this process's capability bounding set - on " +
439
+ 'kernels that enforce the CAP_SETFCAP requirement for mapping uid 0 into a user namespace ' +
440
+ '(Linux 5.12 and distribution backports) every sandboxed command fails while writing a uid ' +
441
+ 'map ("Operation not permitted"). Grant CAP_SETFCAP to this process, or run as a non-root user';
442
+ /**
443
+ * Linux's per-argument cap, MAX_ARG_STRLEN: 32 pages, so 128 KiB on most
444
+ * kernels and up to 2 MiB with 64 KiB pages. The page size is AT_PAGESZ in
445
+ * /proc/self/auxv (pairs of native words); 4 KiB, the smallest, if unreadable.
446
+ */
447
+ let linuxMaxArgStrlen;
448
+ function maxArgStrlen() {
449
+ if (linuxMaxArgStrlen === undefined) {
450
+ const AT_PAGESZ = 6;
451
+ let pageSize = 4096;
452
+ try {
453
+ const auxv = fs.readFileSync('/proc/self/auxv');
454
+ const wordBytes = /64|s390x/.test(process.arch) ? 8 : 4;
455
+ // Buffer reads at most 6 bytes as a number; no value needed here is wider.
456
+ const low = Math.min(wordBytes, 6);
457
+ const word = (at) => endianness() === 'BE'
458
+ ? auxv.readUIntBE(at + wordBytes - low, low)
459
+ : auxv.readUIntLE(at, low);
460
+ for (let at = 0; at + 2 * wordBytes <= auxv.length; at += 2 * wordBytes) {
461
+ if (word(at) === AT_PAGESZ) {
462
+ pageSize = word(at + wordBytes);
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ catch {
468
+ // No /proc: the smallest page size only moves a profile to the file
469
+ // sooner than it had to.
470
+ }
471
+ linuxMaxArgStrlen = 32 * pageSize;
472
+ }
473
+ return linuxMaxArgStrlen;
474
+ }
475
+ /**
476
+ * Room left below the cap when deciding whether the profile stays on the
477
+ * command line: the embedder may put a prefix of its own (`exec`, `cd x &&`,
478
+ * an assignment) in the same argument.
479
+ */
480
+ const ARG_HEADROOM_BYTES = 4096;
481
+ /** bwrap's cap on parsed words, the command line and `--args` file together. */
482
+ const BWRAP_MAX_ARGS = 9000;
483
+ /**
484
+ * The fd the `--args` file is opened on: a single digit, since dash rejects
485
+ * multi-digit redirections, and high, since embedders hand the command low
486
+ * fds of their own (an extra stdio pipe, a helper as `/proc/self/fd/3`).
487
+ */
488
+ const BWRAP_ARGS_FD = 9;
489
+ /**
490
+ * Linux's O_TMPFILE, which neither Node nor Bun exposes in `fs.constants`:
491
+ * the asm-generic __O_TMPFILE, the value on every architecture they build
492
+ * for Linux, together with the O_DIRECTORY the flag is defined to carry — a
493
+ * kernel or filesystem that does not know it therefore fails the open on the
494
+ * directory rather than creating a named file.
495
+ */
496
+ const O_TMPFILE = 0o20000000 | fs.constants.O_DIRECTORY;
497
+ /**
498
+ * The `--args` profiles this process holds open.
499
+ *
500
+ * A profile is an unnamed file (O_TMPFILE): written at wrap time, kept open
501
+ * here, and opened again by the string bwrap runs through
502
+ * `/proc/<pid>/fd/<n>`, which is a fresh read-only description of the same
503
+ * inode at offset 0. Nothing is named at any point, so there is nothing for
504
+ * anyone to substitute between the wrap and the execution — not a sandbox
505
+ * that renames an ancestor of tmpdir, not a sandbox another process of the
506
+ * same user launched with tmpdir writable. Every sandbox this library starts
507
+ * has its own PID namespace and a fresh /proc, so none of them can reach
508
+ * /proc/<this pid> either.
509
+ *
510
+ * Fds close when the sandboxes of a batch are cleaned up, and fd numbers are
511
+ * reused: a string kept past its cleanup and run later opens whatever the
512
+ * number means by then — nothing (the redirection fails and the command does
513
+ * not run), something bwrap refuses, or another profile of this process,
514
+ * never one from outside it.
515
+ */
516
+ const bwrapArgsFds = new Set();
517
+ /** Where the string bwrap runs opens the profile held on `fd`. */
518
+ function bwrapArgsProfilePath(fd) {
519
+ return `/proc/${process.pid}/fd/${fd}`;
520
+ }
521
+ /**
522
+ * Writes `mountWords` NUL-separated to an unnamed file and returns the fd it
523
+ * stays open on. Throws when no directory takes an O_TMPFILE file, or when
524
+ * the profile cannot be opened again through /proc: there is no named-file
525
+ * fallback, and before any of this an over-long profile failed with E2BIG
526
+ * anyway. The check is this process's own open; a child's can still be
527
+ * refused (a process made non-dumpable owns its /proc entries as root), and
528
+ * the string then fails in the redirection and runs no command.
529
+ */
530
+ function openBwrapArgsProfile(mountWords) {
531
+ const contents = mountWords.map(word => word + '\0').join('');
532
+ const failures = [];
533
+ for (const dir of new Set([tmpdir(), '/dev/shm'])) {
534
+ let fd;
535
+ try {
536
+ fd = fs.openSync(dir, O_TMPFILE | fs.constants.O_RDWR, 0o600);
537
+ }
538
+ catch (error) {
539
+ failures.push(`${dir}: ${errorText(error)}`);
540
+ continue;
541
+ }
542
+ try {
543
+ fs.writeFileSync(fd, contents);
544
+ // Read-only from here: this process is done writing it.
545
+ fs.fchmodSync(fd, 0o400);
546
+ // The string opens this path. Fail now, where the caller is told why,
547
+ // rather than when the command runs.
548
+ fs.closeSync(fs.openSync(bwrapArgsProfilePath(fd), fs.constants.O_RDONLY));
549
+ }
550
+ catch (error) {
551
+ failures.push(`${dir}: ${errorText(error)}`);
552
+ fs.closeSync(fd);
553
+ continue;
554
+ }
555
+ bwrapArgsFds.add(fd);
556
+ return fd;
557
+ }
558
+ throw new Error(`no unnamed file could be opened for it (${failures.join('; ')})`);
559
+ }
560
+ function closeBwrapArgsProfile(fd) {
561
+ bwrapArgsFds.delete(fd);
562
+ try {
563
+ fs.closeSync(fd);
564
+ }
565
+ catch {
566
+ // Already closed: nothing left to release.
567
+ }
568
+ }
569
+ function errorText(error) {
570
+ return error instanceof Error ? error.message : String(error);
571
+ }
572
+ /**
573
+ * Thrown when a Linux bubblewrap profile cannot be run on this host: what the
574
+ * configuration expands to is past a limit, or, for `command_too_long` and
575
+ * `nul_in_path`, what the caller passed in is. The command was not run and no
576
+ * profile file stays open, so do not run the per-command cleanup
577
+ * (`cleanupAfterCommand()`, `cleanupBwrapMountPoints()`) for a wrap that
578
+ * threw: it would release a second time, and a sandbox still running would
579
+ * lose its mount points. Branch on `.code`, never on `.message`, which
580
+ * carries the sizes of the moment. Other wrap-time failures (a shell that is
581
+ * not on PATH, a bridge socket that is gone) are plain Errors.
582
+ */
583
+ export class LinuxSandboxProfileError extends Error {
584
+ constructor(code, message, cause) {
585
+ super(message);
586
+ this.name = 'LinuxSandboxProfileError';
587
+ this.code = code;
588
+ if (cause !== undefined) {
589
+ // Non-enumerable, as a native `cause` is. Once the compile target is
590
+ // ES2022 this is `super(message, { cause })`.
591
+ Object.defineProperty(this, 'cause', {
592
+ value: cause,
593
+ writable: true,
594
+ configurable: true,
595
+ enumerable: false,
596
+ });
597
+ }
598
+ }
599
+ }
600
+ /**
601
+ * The shell string that runs bwrap with `bwrapArgs`, which the caller runs
602
+ * as one argument of `sh -c`. When that would not fit the kernel's
603
+ * per-argument cap, the words in `mounts` (a slice of `bwrapArgs`) go to an
604
+ * unnamed file this process holds open (see `bwrapArgsFds`) and bwrap reads
605
+ * them through `--args` at the same position; the string opens that file
606
+ * again through /proc. Every other word, the per-command environment and the
607
+ * command among them, stays on the line. The result stays a simple command,
608
+ * so a prefix (`exec`, `timeout 30`) or a suffix (`&& next`) still composes.
609
+ * Throws {@link LinuxSandboxProfileError} when the profile cannot run.
610
+ */
611
+ function renderBwrapInvocation(bwrapBinary, bwrapArgs, mounts) {
612
+ if (bwrapArgs.length > BWRAP_MAX_ARGS) {
613
+ throw new LinuxSandboxProfileError('too_many_arguments', `Sandbox profile has ${bwrapArgs.length} bwrap arguments and bwrap accepts at most ${BWRAP_MAX_ARGS} (about ${BWRAP_MAX_ARGS / 3} mounts); reduce what the configuration expands to: each path takes about three arguments, each environment variable two`);
614
+ }
615
+ const mountWords = bwrapArgs.slice(mounts.start, mounts.end);
616
+ // A command line ends at a NUL and bwrap splits an args file on one, so the
617
+ // word is cut short on the line and becomes several options in the file.
618
+ if (mountWords.some(word => word.includes('\0'))) {
619
+ throw new LinuxSandboxProfileError('nul_in_path', 'Sandbox profile contains a path with a NUL byte, which neither a command line nor a file of bwrap arguments can carry');
620
+ }
621
+ const inline = quote([bwrapBinary, ...bwrapArgs]);
622
+ const inlineBytes = Buffer.byteLength(inline, 'utf8');
623
+ const limit = maxArgStrlen() - 1;
624
+ if (inlineBytes <= limit - ARG_HEADROOM_BYTES) {
625
+ return inline;
626
+ }
627
+ const tooLong = `Sandbox profile is too long for the command line (${inlineBytes} bytes; past ${limit - ARG_HEADROOM_BYTES} it goes through a file)`;
628
+ // `--args <fd>` are two more words.
629
+ if (bwrapArgs.length + 2 > BWRAP_MAX_ARGS) {
630
+ throw new LinuxSandboxProfileError('too_many_arguments', `${tooLong} and, passed through a file, would exceed the ${BWRAP_MAX_ARGS} arguments bwrap accepts`);
631
+ }
632
+ let argsFd;
633
+ try {
634
+ argsFd = openBwrapArgsProfile(mountWords);
635
+ }
636
+ catch (error) {
637
+ throw new LinuxSandboxProfileError('args_file_unavailable', `${tooLong} and cannot be passed through a file: ${errorText(error)}`, error);
638
+ }
639
+ // /bin/sh opens the profile on the fd and execs bwrap, which reads it to
640
+ // EOF and closes it before running the command.
641
+ const viaArgsFile = quote([
642
+ '/bin/sh',
643
+ '-c',
644
+ `exec ${BWRAP_ARGS_FD}<"$1" && shift && exec "$@"`,
645
+ 'srt-args',
646
+ bwrapArgsProfilePath(argsFd),
647
+ bwrapBinary,
648
+ ...bwrapArgs.slice(0, mounts.start),
649
+ '--args',
650
+ String(BWRAP_ARGS_FD),
651
+ ...bwrapArgs.slice(mounts.end),
652
+ ]);
653
+ const viaArgsFileBytes = Buffer.byteLength(viaArgsFile, 'utf8');
654
+ if (viaArgsFileBytes > limit) {
655
+ closeBwrapArgsProfile(argsFd);
656
+ throw new LinuxSandboxProfileError('command_too_long', `Sandboxed command is too long for one shell argument even with the mounts passed through a file (${viaArgsFileBytes} bytes; the limit here is ${limit})`);
657
+ }
658
+ logForDebugging(`[Sandbox Linux] bwrap mounts moved to an unnamed file, read through ${bwrapArgsProfilePath(argsFd)} on bwrap's fd ${BWRAP_ARGS_FD}: the command line would be ${inlineBytes} bytes as one argument`);
659
+ return viaArgsFile;
660
+ }
661
+ // Number of wrapped commands that have been generated but whose cleanup has
662
+ // not yet run. cleanupBwrapMountPoints() defers file deletion while this is
663
+ // positive, because deleting a mount point file on the host while another
664
+ // bwrap instance is still running detaches that instance's bind mount and
665
+ // the deny rule stops applying inside it.
666
+ let activeSandboxCount = 0;
667
+ let exitHandlerRegistered = false;
668
+ /**
669
+ * Register cleanup handler for bwrap mount points
670
+ */
671
+ function registerExitCleanupHandler() {
672
+ if (exitHandlerRegistered) {
673
+ return;
674
+ }
675
+ process.on('exit', () => {
676
+ cleanupBwrapMountPoints({ force: true });
677
+ });
678
+ exitHandlerRegistered = true;
679
+ }
680
+ /**
681
+ * Clean up mount point files created by bwrap for non-existent deny paths.
682
+ *
683
+ * When protecting non-existent deny paths, bwrap creates empty files on the
684
+ * host filesystem as mount points for --ro-bind. These files persist after
685
+ * bwrap exits. This function removes them.
686
+ *
687
+ * This should be called after each sandboxed command completes to prevent
688
+ * ghost dotfiles (e.g. .bashrc, .gitconfig) from appearing in the working
689
+ * directory. It is also called automatically on process exit as a safety net.
690
+ *
691
+ * Each call decrements the active-sandbox counter that was incremented by
692
+ * wrapCommandWithSandboxLinux(). File deletion is deferred until the counter
693
+ * reaches zero. Deleting a mount point file on the host while another bwrap
694
+ * instance is still running detaches that instance's bind mount (the dentry
695
+ * is unhashed, so path lookup no longer finds the mount) and the deny rule
696
+ * stops applying inside that sandbox.
697
+ *
698
+ * Pass `{ force: true }` to delete unconditionally — used by the process-exit
699
+ * handler and reset() where deferral is not meaningful.
700
+ *
701
+ * Also closes the `--args` profiles the wraps of this batch opened.
702
+ */
703
+ export function cleanupBwrapMountPoints(opts) {
704
+ if (!opts?.force) {
705
+ if (activeSandboxCount > 0) {
706
+ activeSandboxCount--;
707
+ }
708
+ if (activeSandboxCount > 0) {
709
+ logForDebugging(`[Sandbox Linux] Deferring mount point cleanup — ${activeSandboxCount} sandbox(es) still active`);
710
+ return;
711
+ }
712
+ }
713
+ else {
714
+ activeSandboxCount = 0;
715
+ }
716
+ for (const mountPoint of bwrapMountPoints) {
717
+ try {
718
+ // Only remove if it's still the empty file/directory bwrap created.
719
+ // If something else has written real content, leave it alone.
720
+ const stat = fs.statSync(mountPoint);
721
+ if (stat.isFile() && stat.size === 0) {
722
+ fs.unlinkSync(mountPoint);
723
+ logForDebugging(`[Sandbox Linux] Cleaned up bwrap mount point (file): ${mountPoint}`);
724
+ }
725
+ else if (stat.isDirectory()) {
726
+ // Empty directory mount points are created for intermediate
727
+ // components (Fix 2). Only remove if still empty.
728
+ const entries = fs.readdirSync(mountPoint);
729
+ if (entries.length === 0) {
730
+ fs.rmdirSync(mountPoint);
731
+ logForDebugging(`[Sandbox Linux] Cleaned up bwrap mount point (dir): ${mountPoint}`);
732
+ }
733
+ }
734
+ }
735
+ catch {
736
+ // Ignore cleanup errors — the file may have already been removed
737
+ }
738
+ }
739
+ bwrapMountPoints.clear();
740
+ if (emptyMountSourceDir !== undefined) {
741
+ try {
742
+ // rmdirSync, not a recursive remove: it neither follows a symlink nor
743
+ // descends, so a path that is no longer our empty directory is left
744
+ // exactly as found.
745
+ fs.rmdirSync(emptyMountSourceDir);
746
+ logForDebugging(`[Sandbox Linux] Cleaned up the empty-directory mount source: ${emptyMountSourceDir}`);
747
+ emptyMountSourceDir = undefined;
748
+ }
749
+ catch (err) {
750
+ const code = err.code;
751
+ if (code === 'ENOTEMPTY') {
752
+ logForDebugging(`[Sandbox Linux] Left the empty-directory mount source behind, something has written into it: ${emptyMountSourceDir}`);
753
+ }
754
+ // Forget the path only when there is nothing left to remove. ENOTEMPTY,
755
+ // EACCES and EBUSY (a forced cleanup while a live sandbox still pins the
756
+ // source) leave it ours to try again; the revalidation on next use
757
+ // decides whether it can be reused.
758
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
759
+ emptyMountSourceDir = undefined;
760
+ }
761
+ }
762
+ }
763
+ for (const argsFd of [...bwrapArgsFds]) {
764
+ closeBwrapArgsProfile(argsFd);
765
+ }
766
+ }
767
+ function isExecutable(p) {
768
+ try {
769
+ fs.accessSync(p, fs.constants.X_OK);
770
+ return true;
771
+ }
772
+ catch {
773
+ return false;
774
+ }
775
+ }
776
+ /**
777
+ * Get detailed status of Linux sandbox dependencies
778
+ */
779
+ export function getLinuxDependencyStatus(opts) {
780
+ const { seccompConfig, bwrapPath, socatPath } = opts ?? {};
781
+ // argv0 mode: apply-seccomp is compiled into the caller's binary — skip
782
+ // the on-disk lookup and trust that applyPath resolves inside bwrap.
783
+ return {
784
+ hasBwrap: bwrapPath ? isExecutable(bwrapPath) : whichSync('bwrap') !== null,
785
+ hasSocat: socatPath ? isExecutable(socatPath) : whichSync('socat') !== null,
786
+ hasSeccompApply: seccompConfig?.argv0
787
+ ? true
788
+ : getApplySeccompBinaryPath(seccompConfig?.applyPath) !== null,
789
+ };
790
+ }
791
+ /**
792
+ * Check sandbox dependencies and return structured result
793
+ */
794
+ export function checkLinuxDependencies(opts) {
795
+ const { seccompConfig, bwrapPath, socatPath } = opts ?? {};
796
+ const errors = [];
797
+ const warnings = [];
798
+ // An explicit override is a directive, not a hint — if it doesn't exist,
799
+ // surface that rather than silently falling back to PATH.
800
+ let usableBwrap = null;
801
+ if (bwrapPath) {
802
+ if (isExecutable(bwrapPath))
803
+ usableBwrap = bwrapPath;
804
+ else
805
+ errors.push(`bubblewrap (bwrap) not executable at ${bwrapPath}`);
806
+ }
807
+ else {
808
+ usableBwrap = whichSync('bwrap');
809
+ if (usableBwrap === null)
810
+ errors.push('bubblewrap (bwrap) not installed');
811
+ }
812
+ if (socatPath) {
813
+ if (!isExecutable(socatPath))
814
+ errors.push(`socat not executable at ${socatPath}`);
815
+ }
816
+ else if (whichSync('socat') === null) {
817
+ errors.push('socat not installed');
818
+ }
819
+ if (!seccompConfig?.argv0 &&
820
+ getApplySeccompBinaryPath(seccompConfig?.applyPath) === null) {
821
+ warnings.push('seccomp not available - unix socket access not restricted');
822
+ }
823
+ const uid0Error = uid0SandboxError({
824
+ euid: process.geteuid?.(),
825
+ hasSetfcap: processHasBoundingCapability(CAP_SETFCAP),
826
+ bwrap: usableBwrap,
827
+ });
828
+ if (uid0Error !== null)
829
+ errors.push(uid0Error);
830
+ return { warnings, errors };
831
+ }
832
+ /**
833
+ * Predict, then confirm. The prediction — uid 0 with no CAP_SETFCAP to hand
834
+ * to bwrap — is exact about the capability but not about the kernel, so ask
835
+ * bubblewrap itself before failing the caller's initialize(). An error, not a
836
+ * warning: if it holds, every sandboxed command fails.
837
+ *
838
+ * `bwrap === null` skips the probe: a missing or unusable bubblewrap is
839
+ * already its own dependency error, and a second one about capabilities would
840
+ * only misdirect.
841
+ */
842
+ export function uid0SandboxError({ euid, hasSetfcap, bwrap, }) {
843
+ if (euid !== 0 || hasSetfcap || bwrap === null)
844
+ return null;
845
+ const refusal = probeUid0UserNamespace(bwrap);
846
+ if (refusal === null)
847
+ return null;
848
+ return refusal === ''
849
+ ? CAP_SETFCAP_MISSING_MESSAGE
850
+ : `${CAP_SETFCAP_MISSING_MESSAGE} (bubblewrap: ${refusal})`;
851
+ }
852
+ // One probe result per bwrap binary: whether a uid-0 map is refused is a
853
+ // property of the kernel and the binary, not of the moment. Keyed by path so
854
+ // a caller that passes an explicit bwrapPath is not answered for another one.
855
+ const uid0UserNamespaceProbes = new Map();
856
+ /**
857
+ * Run `bwrap --unshare-user --dev-bind / / true` once and report whether this
858
+ * kernel actually refuses to map uid 0. `null` means it does not, so there is
859
+ * nothing to report; otherwise bubblewrap's own first stderr line, or the
860
+ * empty string when the probe could not be run at all and the prediction
861
+ * stands unaided.
862
+ */
863
+ function probeUid0UserNamespace(bwrap) {
864
+ const cached = uid0UserNamespaceProbes.get(bwrap);
865
+ if (cached !== undefined)
866
+ return cached;
867
+ const probe = spawnSync(bwrap, ['--unshare-user', '--dev-bind', '/', '/', 'true'], {
868
+ timeout: 5000,
869
+ stdio: ['ignore', 'ignore', 'pipe'],
870
+ encoding: 'utf8',
871
+ });
872
+ const refusal = probe.error === undefined && probe.status === 0
873
+ ? null
874
+ : ((probe.stderr ?? '')
875
+ .split('\n')
876
+ .map(line => line.trim())
877
+ .find(line => line.length > 0)
878
+ ?.slice(0, 200) ?? '');
879
+ uid0UserNamespaceProbes.set(bwrap, refusal);
880
+ return refusal;
881
+ }
882
+ /**
883
+ * Initialize the Linux network bridge for sandbox networking
884
+ *
885
+ * ARCHITECTURE NOTE:
886
+ * Linux network sandboxing uses bwrap --unshare-net which creates a completely isolated
887
+ * network namespace with NO network access. To enable network access, we:
888
+ *
889
+ * 1. Host side: Run socat bridges that listen on Unix sockets and forward to host proxy servers
890
+ * - HTTP bridge: Unix socket -> host HTTP proxy (for HTTP/HTTPS traffic)
891
+ * - SOCKS bridge: Unix socket -> host SOCKS5 proxy (for SSH/git traffic)
892
+ *
893
+ * 2. Sandbox side: Bind the Unix sockets into the isolated namespace and run socat listeners
894
+ * - HTTP listener on port 3128 -> HTTP Unix socket -> host HTTP proxy
895
+ * - SOCKS listener on port 1080 -> SOCKS Unix socket -> host SOCKS5 proxy
896
+ *
897
+ * 3. Configure environment:
898
+ * - HTTP_PROXY=http://localhost:3128 for HTTP/HTTPS tools
899
+ * - GIT_SSH_COMMAND with socat for SSH through SOCKS5
900
+ *
901
+ * LIMITATION: Unlike macOS sandbox which can enforce domain-based allowlists at the kernel level,
902
+ * Linux's --unshare-net provides only all-or-nothing network isolation. Domain filtering happens
903
+ * at the host proxy level, not the sandbox boundary. This means network restrictions on Linux
904
+ * depend on the proxy's filtering capabilities.
905
+ *
906
+ * DEPENDENCIES: Requires bwrap (bubblewrap) and socat
907
+ */
908
+ export async function initializeLinuxNetworkBridge(httpProxyPort, socksProxyPort, socatPath) {
909
+ const socat = socatPath ?? 'socat';
910
+ const socketId = randomBytes(8).toString('hex');
911
+ const httpSocketPath = join(tmpdir(), `claude-http-${socketId}.sock`);
912
+ // Only allocated when ports differ; in the mux case the SOCKS side
913
+ // reuses httpSocketPath.
914
+ const socksSocketPath = join(tmpdir(), `claude-socks-${socketId}.sock`);
915
+ // Start HTTP bridge
916
+ const httpSocatArgs = [
917
+ `UNIX-LISTEN:${httpSocketPath},fork,reuseaddr`,
918
+ `TCP:localhost:${httpProxyPort},keepalive,keepidle=10,keepintvl=5,keepcnt=3`,
919
+ ];
920
+ logForDebugging(`Starting HTTP bridge: ${socat} ${httpSocatArgs.join(' ')}`);
921
+ const httpBridgeProcess = spawn(socat, httpSocatArgs, {
922
+ stdio: 'ignore',
923
+ });
924
+ // Add error and exit handlers to monitor bridge health. These must be
925
+ // registered before the !pid check: when spawn fails (e.g. socat is
926
+ // missing or not executable), the ChildProcess emits an asynchronous
927
+ // 'error' event, and throwing first would leave that event without a
928
+ // listener — surfacing as an uncaughtException instead of the rejection
929
+ // below.
930
+ httpBridgeProcess.on('error', err => {
931
+ logForDebugging(`HTTP bridge process error: ${err}`, { level: 'error' });
932
+ });
933
+ httpBridgeProcess.on('exit', (code, signal) => {
934
+ logForDebugging(`HTTP bridge process exited with code ${code}, signal ${signal}`, { level: code === 0 ? 'info' : 'error' });
935
+ });
936
+ if (!httpBridgeProcess.pid) {
937
+ throw new Error('Failed to start HTTP bridge process');
938
+ }
939
+ // SOCKS bridge: when the host serves both protocols on one port (the mux),
940
+ // a second socat to the same TCP target is redundant — reuse the HTTP
941
+ // bridge's process and socket path. Downstream consumers
942
+ // (LinuxNetworkBridgeContext, in-sandbox socat, cleanup) treat duplicate
943
+ // refs idempotently. A separate bridge is only spawned when the ports
944
+ // differ (external proxy override).
945
+ let socksBridgeProcess;
946
+ let socksSockPath;
947
+ if (socksProxyPort === httpProxyPort) {
948
+ socksBridgeProcess = httpBridgeProcess;
949
+ socksSockPath = httpSocketPath;
950
+ }
951
+ else {
952
+ socksSockPath = socksSocketPath;
953
+ const socksSocatArgs = [
954
+ `UNIX-LISTEN:${socksSocketPath},fork,reuseaddr`,
955
+ `TCP:localhost:${socksProxyPort},keepalive,keepidle=10,keepintvl=5,keepcnt=3`,
956
+ ];
957
+ logForDebugging(`Starting SOCKS bridge: ${socat} ${socksSocatArgs.join(' ')}`);
958
+ socksBridgeProcess = spawn(socat, socksSocatArgs, {
959
+ stdio: 'ignore',
960
+ });
961
+ // Add error and exit handlers to monitor bridge health — registered
962
+ // before the !pid check for the same reason as the HTTP bridge above.
963
+ socksBridgeProcess.on('error', err => {
964
+ logForDebugging(`SOCKS bridge process error: ${err}`, { level: 'error' });
965
+ });
966
+ socksBridgeProcess.on('exit', (code, signal) => {
967
+ logForDebugging(`SOCKS bridge process exited with code ${code}, signal ${signal}`, { level: code === 0 ? 'info' : 'error' });
968
+ });
969
+ if (!socksBridgeProcess.pid) {
970
+ // Clean up HTTP bridge
971
+ if (httpBridgeProcess.pid) {
972
+ try {
973
+ process.kill(httpBridgeProcess.pid, 'SIGTERM');
974
+ }
975
+ catch {
976
+ // Ignore errors
977
+ }
978
+ }
979
+ throw new Error('Failed to start SOCKS bridge process');
980
+ }
981
+ }
982
+ // Wait for both sockets to be ready
983
+ const maxAttempts = 5;
984
+ for (let i = 0; i < maxAttempts; i++) {
985
+ if (!httpBridgeProcess.pid ||
986
+ httpBridgeProcess.killed ||
987
+ !socksBridgeProcess.pid ||
988
+ socksBridgeProcess.killed) {
989
+ throw new Error('Linux bridge process died unexpectedly');
990
+ }
991
+ try {
992
+ // fs already imported
993
+ if (fs.existsSync(httpSocketPath) && fs.existsSync(socksSockPath)) {
994
+ logForDebugging(`Linux bridges ready after ${i + 1} attempts`);
995
+ break;
996
+ }
997
+ }
998
+ catch (err) {
999
+ logForDebugging(`Error checking sockets (attempt ${i + 1}): ${err}`, {
1000
+ level: 'error',
1001
+ });
1002
+ }
1003
+ if (i === maxAttempts - 1) {
1004
+ // Clean up both processes
1005
+ if (httpBridgeProcess.pid) {
1006
+ try {
1007
+ process.kill(httpBridgeProcess.pid, 'SIGTERM');
1008
+ }
1009
+ catch {
1010
+ // Ignore errors
1011
+ }
1012
+ }
1013
+ if (socksBridgeProcess.pid) {
1014
+ try {
1015
+ process.kill(socksBridgeProcess.pid, 'SIGTERM');
1016
+ }
1017
+ catch {
1018
+ // Ignore errors
1019
+ }
1020
+ }
1021
+ throw new Error(`Failed to create bridge sockets after ${maxAttempts} attempts`);
1022
+ }
1023
+ await new Promise(resolve => setTimeout(resolve, i * 100));
1024
+ }
1025
+ return {
1026
+ httpSocketPath,
1027
+ socksSocketPath: socksSockPath,
1028
+ httpBridgeProcess,
1029
+ socksBridgeProcess,
1030
+ httpProxyPort,
1031
+ socksProxyPort,
1032
+ };
1033
+ }
1034
+ /**
1035
+ * Resolve how to invoke apply-seccomp: either a standalone binary path, or a
1036
+ * multicall-binary prefix that dispatches on the ARGV0 env var.
1037
+ *
1038
+ * Returns a shell-ready string ending in a trailing space — callers append
1039
+ * quote([shell, '-c', cmd]). Returns undefined when seccomp is
1040
+ * unavailable (no argv0, no binary found).
1041
+ *
1042
+ * When argv0 is set, applyPath is used verbatim (no existence check); the
1043
+ * caller is responsible for ensuring it resolves inside the bwrap namespace.
1044
+ */
1045
+ function resolveApplySeccompPrefix(applyPath, argv0) {
1046
+ if (argv0) {
1047
+ if (!applyPath) {
1048
+ throw new Error('seccompConfig.argv0 requires seccompConfig.applyPath');
1049
+ }
1050
+ return `ARGV0=${quote([argv0])} ${quote([applyPath])} `;
1051
+ }
1052
+ const binary = getApplySeccompBinaryPath(applyPath);
1053
+ return binary ? `${quote([binary])} ` : undefined;
1054
+ }
1055
+ /**
1056
+ * Build the command that runs inside the sandbox.
1057
+ * Sets up HTTP proxy on port 3128 and SOCKS proxy on port 1080
1058
+ */
1059
+ function buildSandboxCommand(httpSocketPath, socksSocketPath, userCommand, applySeccompPrefix, shell, socatPath) {
1060
+ // Default to bash for backward compatibility
1061
+ const shellPath = shell || 'bash';
1062
+ // Host filesystem is bind-mounted into the sandbox, so an explicit
1063
+ // socatPath resolves to the same binary inside bwrap.
1064
+ const socat = quote([socatPath ?? 'socat']);
1065
+ const socatCommands = [
1066
+ `${socat} TCP-LISTEN:3128,fork,reuseaddr UNIX-CONNECT:${httpSocketPath} >/dev/null 2>&1 &`,
1067
+ `${socat} TCP-LISTEN:1080,fork,reuseaddr UNIX-CONNECT:${socksSocketPath} >/dev/null 2>&1 &`,
1068
+ // The trap saves the status the script is exiting with and exits with
1069
+ // it. A bare `exit` inside an EXIT trap is not portable: bash and dash
1070
+ // keep the script's status, zsh takes the status of the trap's own last
1071
+ // command (the kill), so under zsh a failing command reported 0. Single
1072
+ // quotes, so $? and $rc are read when the trap runs, not when it is set.
1073
+ "trap 'rc=$?; kill %1 %2 2>/dev/null; exit $rc' EXIT",
1074
+ ];
1075
+ // apply-seccomp runs after socat so socat can still create Unix sockets.
1076
+ if (applySeccompPrefix) {
1077
+ const applySeccompCmd = applySeccompPrefix + quote([shellPath, '-c', userCommand]);
1078
+ const innerScript = [...socatCommands, applySeccompCmd].join('\n');
1079
+ return `${shellPath} -c ${quote([innerScript])}`;
1080
+ }
1081
+ else {
1082
+ const innerScript = [...socatCommands, `eval ${quote([userCommand])}`].join('\n');
1083
+ return `${shellPath} -c ${quote([innerScript])}`;
1084
+ }
1085
+ }
1086
+ /**
1087
+ * The three classes an fs errno from a path lookup falls into, enumerated
1088
+ * once because each leads somewhere different: ABSENCE, where the name
1089
+ * resolves to no file and every consumer already treats the path as absent
1090
+ * (isAbsenceErrno, in sandbox-utils.ts, where the glob walk classifies the
1091
+ * same failures); TRANSIENT, a busy, unlucky or re-exporting host rather
1092
+ * than a fact about the path, worth asking once more before the failure is
1093
+ * taken for an answer; and SETTLED, a file that is there and cannot be
1094
+ * looked at, and will not become readable while this wrap runs. Every code
1095
+ * neither predicate names is settled, because nothing says it clears.
1096
+ */
1097
+ function isTransientErrno(err) {
1098
+ const code = err?.code;
1099
+ return (code === 'EIO' ||
1100
+ code === 'ESTALE' ||
1101
+ code === 'EMFILE' ||
1102
+ code === 'ENFILE' ||
1103
+ code === 'EAGAIN');
1104
+ }
1105
+ /**
1106
+ * `attempt`, asked a second time when the first failure was transient and
1107
+ * rethrown otherwise. Two caveats come with the second ask: a same-tick retry
1108
+ * cannot clear descriptor exhaustion (EMFILE, ENFILE), so for those it is
1109
+ * best effort. And on a soft-mounted network filesystem that is down, a
1110
+ * transient-class errno arrives only after the mount's own timeout, so the
1111
+ * retry can double that stall — a hard mount never returns at all, so the
1112
+ * retry never runs there.
1113
+ */
1114
+ function retryingTransient(attempt) {
1115
+ try {
1116
+ return attempt();
1117
+ }
1118
+ catch (err) {
1119
+ if (!isTransientErrno(err))
1120
+ throw err;
1121
+ }
1122
+ return attempt();
1123
+ }
1124
+ /** The top-level directories this wrap leaves to the kernel and to the
1125
+ * caller's own remounts. A cover must never take one — --proc and --dev
1126
+ * replace two of them after the pins are spliced in, and /sys is kernel
1127
+ * state — and the '/' read-deny expansion must not hide one either. */
1128
+ const KERNEL_TOP_LEVEL_DIRS = ['/proc', '/dev', '/sys'];
1129
+ /**
1130
+ * A self-bind ("pin") for every directory between a seed (where a deny bind,
1131
+ * file mask or read-deny tmpfs lands) and the allowed write root covering it,
1132
+ * shallow-first, which makes each one a mountpoint the kernel refuses to
1133
+ * rename or remove. A directory that is not there is skipped: bwrap cannot
1134
+ * bind a missing source and there is nothing to rename. One that exists but
1135
+ * cannot be inspected is still pinned, and if bwrap cannot bind it either the
1136
+ * sandbox does not start, which is where the deny loop lands too.
1137
+ *
1138
+ * Pins are read-only and spliced in beneath every other mount, so that one
1139
+ * landing on top of an allowed write root cannot make it read-only; a write
1140
+ * root needs no pin of its own there, its allow bind already being a live
1141
+ * mountpoint.
1142
+ *
1143
+ * A '/' write root defeats that: its own recursive `--bind / /` is one of the
1144
+ * mounts above the pins and buries them, leaving those directories renamable.
1145
+ * Under one the pins therefore go AFTER the allow binds and take every
1146
+ * ancestor, the other allowed write roots included, stopping below the
1147
+ * top-level directory D on the chain; a writable `--bind D D` — the cover —
1148
+ * takes D itself and buries them again. Lookups enter the cover and never meet
1149
+ * a pin, so no filesystem boundary lands inside D, while the kernel's rename
1150
+ * and rmdir checks scan mounts by dentry and still find one. The only new
1151
+ * boundary is at D. /proc, /dev and /sys are never covered, and a seed under
1152
+ * one of them gets no pins.
1153
+ */
1154
+ function ancestorPinArgs(seeds, writeRoots) {
1155
+ const { rootIsWriteRoot } = writeRoots;
1156
+ const pinDirs = new Set();
1157
+ const coverDirs = new Set();
1158
+ // Verdicts are seed-independent: a visited ancestor's chain is done.
1159
+ const visited = new Set();
1160
+ for (const seed of seeds) {
1161
+ if (rootIsWriteRoot &&
1162
+ KERNEL_TOP_LEVEL_DIRS.some(dir => isAtOrUnder(seed, dir))) {
1163
+ continue;
1164
+ }
1165
+ for (let dir = path.dirname(seed); dir !== '/' && !visited.has(dir) && writeRoots.isWithinAllowedWrite(dir); dir = path.dirname(dir)) {
1166
+ visited.add(dir);
1167
+ if (rootIsWriteRoot) {
1168
+ if (path.dirname(dir) === '/') {
1169
+ coverDirs.add(dir);
1170
+ continue;
1171
+ }
1172
+ }
1173
+ else if (writeRoots.isAllowedWriteRoot(dir)) {
1174
+ continue;
1175
+ }
1176
+ try {
1177
+ fs.statSync(dir);
1178
+ }
1179
+ catch (err) {
1180
+ if (isAbsenceErrno(err))
1181
+ continue;
1182
+ }
1183
+ pinDirs.add(dir);
1184
+ }
1185
+ }
1186
+ const covers = [...coverDirs];
1187
+ if (covers.length > 0) {
1188
+ logForDebugging(`[Sandbox Linux] Covering the pinned ancestors under: ${covers.join(', ')}`);
1189
+ }
1190
+ return [
1191
+ ...[...pinDirs]
1192
+ .sort((a, b) => a.split('/').length - b.split('/').length)
1193
+ .flatMap(dir => ['--ro-bind', dir, dir]),
1194
+ ...covers.flatMap(dir => ['--bind', dir, dir]),
1195
+ ];
1196
+ }
1197
+ /**
1198
+ * Mount a tmpfs over a read-denied directory, then restore the allowed write
1199
+ * paths and allowRead paths the tmpfs just wiped. Returns the mounts it
1200
+ * re-bound, writable and read-only, which is what a later pass re-emits when
1201
+ * a denyWrite bind re-exposes this directory.
1202
+ */
1203
+ function pushReadDenyDirMounts(args, unit) {
1204
+ const { landing, allowedWritePaths, readAllowPaths } = unit;
1205
+ args.push('--tmpfs', landing);
1206
+ // How to bind allowed path `p` back, if it is a carve-out of this unit:
1207
+ // its name must live inside the denied directory, and so must what it
1208
+ // resolves to. Parent directories may be symlinks (/lib/x is a carve-out
1209
+ // of a tmpfs on /usr, where /lib is a link to /usr/lib). A path that is
1210
+ // itself a symlink INTO the directory is not a carve-out: it names the
1211
+ // link, which this tmpfs did not hide, and re-allows nothing the link
1212
+ // points at, or a link planted at an allowed path (docs -> ~/.ssh) would
1213
+ // cancel the deny of its target.
1214
+ //
1215
+ // The mount goes back at the NAME (the entry re-allows the name it is, and
1216
+ // an entry whose last component is a symlink is ENOENT inside the sandbox
1217
+ // if restored only at its target), from the RESOLVED source: that is the
1218
+ // path both containment checks were made against, and naming it leaves
1219
+ // bwrap nothing to re-resolve at mount time.
1220
+ const restorePlacementOf = (p) => {
1221
+ const dest = unit.nameLocation(p);
1222
+ if (!isAtOrUnder(dest, landing))
1223
+ return undefined;
1224
+ const { canonical: source, resolved } = unit.resolve(p);
1225
+ if (!resolved) {
1226
+ logForDebugging(`[Sandbox Linux] Not restoring ${p} over denyRead tmpfs ${landing}: nothing there resolves (absent, dangling or unreadable), so the only source available is the name itself`, { level: 'warn' });
1227
+ return undefined;
1228
+ }
1229
+ if (!isAtOrUnder(source, landing)) {
1230
+ logForDebugging(`[Sandbox Linux] Not restoring ${p} over denyRead tmpfs ${landing}: it resolves outside it, to ${source}`, { level: 'warn' });
1231
+ return undefined;
1232
+ }
1233
+ // Restoring at the name from a DIFFERENT path is a second mount of the
1234
+ // target's inode, which nothing landing on the target's own path covers,
1235
+ // so such a restore is dropped whenever the read section hides anything
1236
+ // at, inside or around that target: the deny wins, and the follow-up that
1237
+ // gives the carve-out back has to bind the target at the target.
1238
+ if (source !== dest) {
1239
+ const denied = unit.readDenialAround(source);
1240
+ if (denied !== undefined) {
1241
+ logForDebugging(`[Sandbox Linux] Not restoring ${p} over denyRead tmpfs ${landing}: it resolves to ${source}, and the read section denies or masks ${denied}`, { level: 'warn' });
1242
+ return undefined;
1243
+ }
1244
+ }
1245
+ return { source, dest };
1246
+ };
1247
+ // tmpfs wiped any earlier write binds under this path — restore them.
1248
+ const restoredWrites = [];
1249
+ for (const writePath of allowedWritePaths) {
1250
+ const placement = restorePlacementOf(writePath);
1251
+ if (placement === undefined)
1252
+ continue;
1253
+ args.push('--bind', placement.source, placement.dest);
1254
+ restoredWrites.push(placement);
1255
+ logForDebugging(`[Sandbox Linux] Re-bound write path wiped by denyRead tmpfs: ${writePath}`);
1256
+ }
1257
+ // Re-allow specific paths within the denied directory (allowRead overrides denyRead).
1258
+ // After mounting tmpfs over the denied dir, bind back the allowed subdirectories
1259
+ // so they are readable again.
1260
+ const restoredReads = [];
1261
+ for (const allowPath of readAllowPaths) {
1262
+ const placement = restorePlacementOf(allowPath);
1263
+ if (placement === undefined)
1264
+ continue;
1265
+ // Skip only if a write path was re-bound just above AND covers
1266
+ // allowPath. A write path that's an ancestor of the deny dir isn't
1267
+ // re-bound (it wasn't wiped), so allowPath under it still needs
1268
+ // its own ro-bind here.
1269
+ if (restoredWrites.some(w => isAtOrUnder(placement.dest, w.dest)))
1270
+ continue;
1271
+ // Bind the allowed path back over the tmpfs so it's readable
1272
+ args.push('--ro-bind', placement.source, placement.dest);
1273
+ restoredReads.push(placement);
1274
+ logForDebugging(`[Sandbox Linux] Re-allowed read access within denied region: ${allowPath}`);
1275
+ }
1276
+ return { restoredWrites, restoredReads };
1277
+ }
1278
+ /**
1279
+ * Generate filesystem bind mount arguments for bwrap
1280
+ */
1281
+ async function generateFilesystemArgs(readConfig, writeConfig, maskedFileBinds, maskedFileStoreDir, ripgrepConfig = { command: 'rg' }, mandatoryDenySearchDepth = DEFAULT_MANDATORY_DENY_SEARCH_DEPTH, allowGitConfig = false, abortSignal) {
1282
+ const args = [];
1283
+ // fs already imported
1284
+ // Collect normalized allowed write paths. Populated in the writeConfig
1285
+ // block, read again in the denyRead loop to re-bind writes under tmpfs.
1286
+ const allowedWritePaths = [];
1287
+ // denyWrite binds are buffered and emitted after denyRead processing so that
1288
+ // a denyRead tmpfs over an ancestor directory doesn't wipe them out.
1289
+ const denyWriteArgs = [];
1290
+ // Directories that a deny entry re-binds read-only inside the sandbox
1291
+ // (--ro-bind <dir> <dir>), keyed by resolved dest, with every raw
1292
+ // (pre-resolution) spelling each was reached through. A non-existent deny
1293
+ // path whose deepest existing ancestor lies within one of these is already
1294
+ // uncreatable, and must not get a /dev/null stub: bwrap would have to
1295
+ // creat() the mount point inside that read-only mount and abort ("Can't
1296
+ // create file at <path>: Read-only file system"). An EXISTING deny path
1297
+ // strictly beneath one is likewise already unwritable and its own
1298
+ // --ro-bind <p> <p> is skipped as redundant. The spellings matter
1299
+ // because the re-application passes record a deny bind's raw spelling
1300
+ // beside its resolved dest, so the stub-skip guard tests a covering
1301
+ // directory in its canonical form AND every recorded spelling.
1302
+ const readOnlyDenyDirSpellings = new Map();
1303
+ // dest → the pre-resolution deny path it came from. A bind at the resolved
1304
+ // dest also re-exposes whatever the symlinked spelling leads to, so the
1305
+ // re-application passes below compare a read deny's landing against both
1306
+ // spellings. Landings and allowed write paths are canonical, so the extra
1307
+ // spelling can only match more of them, never fewer.
1308
+ const denyWriteRawDests = new Map();
1309
+ // The shared empty directory this call's placeholders bind from, resolved at
1310
+ // most once per wrap: resolving again mid-wrap (the cached path having been
1311
+ // tampered with in between) would leave the binds already emitted pointing
1312
+ // at a directory this same call has just judged not ours.
1313
+ let emptySource;
1314
+ // Where a mount given `p` lands: `p` fully resolved, every symlink on the
1315
+ // way and not one hop. One resolution per path per wrap, so every predicate
1316
+ // below sees the same answer, and none before the mandatory-deny scan's
1317
+ // await: that scan can run arbitrarily long, and a realpath taken ahead of
1318
+ // it would miss a symlink retargeted meanwhile.
1319
+ const canonicalFormCache = new Map();
1320
+ // Paths whose canonical location could not be LOOKED AT — a settled errno,
1321
+ // or a transient one that outlived the retry — so the recorded spelling
1322
+ // stands in for it. That is a guess, and the
1323
+ // stub-skip prediction below refuses to conclude anything from a guess made
1324
+ // about its own inputs. Plain absence is not a guess: nothing is there to
1325
+ // resolve, and every consumer already treats such a path as absent (a
1326
+ // dangling symlink under '/' is ordinary, and counting it would keep every
1327
+ // deny placeholder on the whole host).
1328
+ const canonicalFormGuesses = new Set();
1329
+ // Paths canonicalForm answered with the spelling it was given because
1330
+ // nothing could be resolved. A mount SOURCE must never be one of them:
1331
+ // bwrap would resolve whatever sits at that name at mount time, after the
1332
+ // containment checks ran against the name itself.
1333
+ const canonicalFormUnresolved = new Set();
1334
+ const canonicalForm = (p) => {
1335
+ let canonical = canonicalFormCache.get(p);
1336
+ if (canonical === undefined) {
1337
+ try {
1338
+ canonical = retryingTransient(() => fs.realpathSync(p));
1339
+ }
1340
+ catch (err) {
1341
+ canonical = p; // vanished or unresolvable: the recorded form stands
1342
+ canonicalFormUnresolved.add(p);
1343
+ if (!isAbsenceErrno(err))
1344
+ canonicalFormGuesses.add(p);
1345
+ }
1346
+ canonicalFormCache.set(p, canonical);
1347
+ }
1348
+ return canonical;
1349
+ };
1350
+ /** Where `p` resolves to, beside whether anything was resolved at all.
1351
+ * Answered together because the answer is the spelling itself when nothing
1352
+ * was, and a mount source must never be one of those. */
1353
+ const canonicalLocationOf = (p) => ({
1354
+ canonical: canonicalForm(p),
1355
+ resolved: !canonicalFormUnresolved.has(p),
1356
+ });
1357
+ /** `p` as recorded, plus its canonical location when that differs. */
1358
+ const mountForms = (p) => {
1359
+ const canonical = canonicalForm(p);
1360
+ return canonical === p ? [p] : [p, canonical];
1361
+ };
1362
+ // Where the name `p` lives: its parent directories resolved, its last
1363
+ // component as written. This, not what `p` resolves to, decides which read
1364
+ // deny an allowRead entry is an exception to: an entry that is a symlink
1365
+ // names the link, and re-allows nothing the link points at.
1366
+ const nameLocationOf = (p) => {
1367
+ if (p === '/')
1368
+ return p;
1369
+ const parent = canonicalForm(path.dirname(p));
1370
+ return `${parent === '/' ? '' : parent}/${path.basename(p)}`;
1371
+ };
1372
+ // Whether a canonical path lies inside the write allowlist, and so
1373
+ // whether it is denied, stubbed and pinned at all: a path outside it is
1374
+ // left read-only by the initial --ro-bind / /. The deny pre-pass, the deny
1375
+ // loop's --ro-bind gate and the ancestor-pin walk MUST share it: the
1376
+ // pre-pass is only sound if it records exactly the directories the loop
1377
+ // re-binds read-only (a recorded directory that is never re-bound read-only
1378
+ // would suppress stubs unsafely; a re-bound directory missing from the
1379
+ // record only costs an abort). allowedWritePaths entries are canonical (the
1380
+ // allow loop drops a symlink-spelled one) and recorded with trailing
1381
+ // slashes stripped.
1382
+ //
1383
+ // Containment is root-aware (isAtOrUnder) because '/' is a legal allowOnly
1384
+ // entry that the allow loop binds writable: an `allowedPath + '/'` prefix
1385
+ // test spells it '//' and matches nothing, so every deny no other allow
1386
+ // entry covers would be judged outside the allowlist and silently lose its
1387
+ // bind over a writable root. The ancestor pins are the one consumer that
1388
+ // treats a '/' write root specially: see ancestorPinArgs.
1389
+ const isWithinAnyAllowedWritePath = (candidatePath) => allowedWritePaths.some(allowedPath => isAtOrUnder(candidatePath, allowedPath));
1390
+ const isAllowedWriteRoot = (candidatePath) => allowedWritePaths.includes(candidatePath);
1391
+ // Resolves symlinks, so computed on first use, which is after the
1392
+ // mandatory-deny scan.
1393
+ let readAllowPathsMemo;
1394
+ const readAllowPaths = () => (readAllowPathsMemo ?? (readAllowPathsMemo = (readConfig?.allowWithinDeny || []).map(p => normalizePathForSandbox(p))));
1395
+ // What the read section mounts for one denyRead entry: a tmpfs (directory)
1396
+ // or a /dev/null mask (anything else) on the entry itself; nothing when it
1397
+ // is not there; and, when it cannot be inspected, a tmpfs on the deepest
1398
+ // directory above it that can. That hides more than was asked, never less:
1399
+ // a same-uid command can make a parent unsearchable (chmod 000) and undo it
1400
+ // again from inside the next sandbox, so "unreadable now" is not "absent".
1401
+ // This is the widest blast radius a transient failure has, hence the retry:
1402
+ // one unlucky EIO turns a file deny into a stand-in over its whole
1403
+ // directory, and every carve-out beneath that is then refused.
1404
+ const readDenyTargetOf = (entry) => {
1405
+ for (let candidate = entry;; candidate = path.dirname(candidate)) {
1406
+ if (candidate === '/' && candidate !== entry)
1407
+ return undefined;
1408
+ try {
1409
+ const stats = retryingTransient(() => fs.statSync(candidate));
1410
+ return {
1411
+ path: candidate,
1412
+ isDirectory: stats.isDirectory(),
1413
+ isStandIn: candidate !== entry,
1414
+ };
1415
+ }
1416
+ catch (err) {
1417
+ if (isAbsenceErrno(err) || candidate === '/')
1418
+ return undefined;
1419
+ }
1420
+ }
1421
+ };
1422
+ // Where that mount lands, which is where the entry really is: one mount
1423
+ // then covers every spelling of that place; the real contents are hidden
1424
+ // wherever they are reachable, even when the route that named them is
1425
+ // itself inside an earlier tmpfs; and no mount is ever asked to land on a
1426
+ // symlink, which bubblewrap refuses outright from 0.12 on ("Can't mount on
1427
+ // symlink destination").
1428
+ //
1429
+ // '/' is never a landing. Only a symlink to the root resolves there (a '/'
1430
+ // entry itself is expanded into the root's children), and a --tmpfs /
1431
+ // would wipe every mount placed before it while the pivot promoted it,
1432
+ // booting the command on an empty tree. The directory holding the link
1433
+ // stands in for it, on the same rule as an entry that cannot be inspected,
1434
+ // and `undefined` means nothing at all is mounted for this entry.
1435
+ const readDenyMountOf = (entry) => {
1436
+ const target = readDenyTargetOf(entry);
1437
+ if (target === undefined)
1438
+ return undefined;
1439
+ const landing = canonicalForm(target.path);
1440
+ if (landing !== '/') {
1441
+ return {
1442
+ landing,
1443
+ named: target.path,
1444
+ isDirectory: target.isDirectory,
1445
+ isStandIn: target.isStandIn,
1446
+ };
1447
+ }
1448
+ const holder = canonicalForm(path.dirname(target.path));
1449
+ if (holder === '/')
1450
+ return undefined;
1451
+ return {
1452
+ landing: holder,
1453
+ named: target.path,
1454
+ isDirectory: true,
1455
+ isStandIn: true,
1456
+ };
1457
+ };
1458
+ // The directories and files the read section denies, as configured. A '/'
1459
+ // entry is expanded into the root's children, because --tmpfs / would wipe
1460
+ // every prior mount (ro-bind /, write binds, deny binds). /proc and /dev are
1461
+ // skipped (the caller remounts them after this function returns) and so is
1462
+ // /sys (kernel interface; the host's is already read-only via ro-bind). A
1463
+ // child an allowRead entry covers (its name is the child, or an ancestor
1464
+ // of where the child lands) is skipped too: the deny is synthetic and the
1465
+ // allow is the caller's, and a tmpfs at the child's canonical location
1466
+ // (/bin lands at /usr/bin) would only be restored over again. An entry
1467
+ // that is a symlink to the child covers nothing: its name lives elsewhere. /etc/ssh/ssh_config.d is
1468
+ // always hidden when there is a read policy: ssh is strict about config
1469
+ // file ownership and permissions, which can look wrong inside the sandbox
1470
+ // ("Bad owner or permissions" under OrbStack). Throws if '/' cannot be
1471
+ // listed, a transient failure having been retried once. Computed on first
1472
+ // use, like readAllowPaths.
1473
+ let readDenyEntriesMemo;
1474
+ const readDenyEntries = () => {
1475
+ if (readDenyEntriesMemo !== undefined)
1476
+ return readDenyEntriesMemo;
1477
+ if (!readConfig)
1478
+ return [];
1479
+ const entries = [];
1480
+ for (const p of readConfig.denyOnly || []) {
1481
+ if (normalizePathForSandbox(p) !== '/') {
1482
+ entries.push(p);
1483
+ continue;
1484
+ }
1485
+ // The only call here that throws, and the list is memoised on success
1486
+ // alone, so the retry belongs to it rather than to one of its callers:
1487
+ // the stub-skip derivation catches a throw and gives up on the
1488
+ // prediction, and the denyRead loop below does not catch one at all.
1489
+ const children = retryingTransient(() => fs.readdirSync('/'));
1490
+ for (const child of children) {
1491
+ if (KERNEL_TOP_LEVEL_DIRS.includes(`/${child}`))
1492
+ continue;
1493
+ const childLocation = canonicalForm('/' + child);
1494
+ const covered = readAllowPaths().some(allowPath => isAtOrUnder(childLocation, nameLocationOf(allowPath)));
1495
+ if (covered) {
1496
+ logForDebugging(`[Sandbox Linux] Root deny expansion skips /${child}: covered by allowRead`);
1497
+ continue;
1498
+ }
1499
+ entries.push('/' + child);
1500
+ }
1501
+ }
1502
+ if (fs.existsSync('/etc/ssh/ssh_config.d')) {
1503
+ entries.push('/etc/ssh/ssh_config.d');
1504
+ }
1505
+ readDenyEntriesMemo = entries;
1506
+ return entries;
1507
+ };
1508
+ // Shallow-first by canonical depth, so a tmpfs over a directory lands
1509
+ // before the tmpfs or /dev/null mask on anything inside it however either
1510
+ // was spelled. That order is what lets a unit restore its write and
1511
+ // allowRead paths unconditionally: nothing emitted so far can lie inside
1512
+ // one. Sorted by spelling, a symlink-spelled entry could mount first inside
1513
+ // a directory listed after it, whose restores would then bury it.
1514
+ const canonicalDepth = (p) => canonicalForm(p).split('/').length;
1515
+ // What every denyRead entry mounts and where: ONE walk, whose answers the
1516
+ // deny loop, the locations the carve-out gate below reads, and the
1517
+ // stub-skip prediction all take, so none of them can disagree about where
1518
+ // a deny lands. `mount` is undefined where nothing is mounted at all;
1519
+ // `liftedFile` marks a file deny an allowRead entry naming that very file
1520
+ // cancels — the entry mounts nothing in that case either. Lazy and
1521
+ // memoised like readDenyEntries(): it resolves symlinks, so it must not run
1522
+ // before the mandatory-deny scan's await.
1523
+ let readDenyPlanMemo;
1524
+ const readDenyPlan = () => (readDenyPlanMemo ?? (readDenyPlanMemo = readDenyEntries()
1525
+ .map(p => normalizePathForSandbox(p))
1526
+ .sort((a, b) => canonicalDepth(a) - canonicalDepth(b))
1527
+ .map(normalizedPath => {
1528
+ const mount = readDenyMountOf(normalizedPath);
1529
+ return {
1530
+ normalizedPath,
1531
+ mount,
1532
+ liftedFile: mount !== undefined &&
1533
+ !mount.isDirectory &&
1534
+ readAllowPaths().some(allowPath => nameLocationOf(allowPath) === mount.landing),
1535
+ };
1536
+ })));
1537
+ // Where the ancestor pins are spliced in once every mount is known:
1538
+ // beneath the allow binds, or after them under a '/' write root that would
1539
+ // otherwise bury them (see ancestorPinArgs).
1540
+ let ancestorPinInsertAt;
1541
+ // Determine initial root mount based on write restrictions
1542
+ if (writeConfig) {
1543
+ // Write restrictions: Start with read-only root, then allow writes to specific paths
1544
+ args.push('--ro-bind', '/', '/');
1545
+ const beneathAllowBinds = args.length;
1546
+ // Allow writes to specific paths
1547
+ for (const pathPattern of writeConfig.allowOnly || []) {
1548
+ // normalizePathForSandbox already strips a trailing slash from every
1549
+ // spelling it does not take for a glob; this strip covers the ones it
1550
+ // exempts — a literal directory named with glob characters, spelled
1551
+ // '<dir>/[id]/'. Allow paths are recorded slash-free because every
1552
+ // downstream comparison — the deny loop's within-allowlist gate,
1553
+ // findSymlinkInPath's mask scoping, the emission filter's re-expose
1554
+ // check, the denyRead re-bind and its allowRead skip, and the stub-skip
1555
+ // vetoes — matches by `allowedPath + '/'` prefix, which '<dir>//'
1556
+ // silently defeats. bwrap binds 'dir' and 'dir/' identically, so
1557
+ // normalizing the recorded spelling fixes every consumer at once
1558
+ // instead of per-predicate. ('/' itself is kept; the empty spelling an
1559
+ // empty $HOME expands '~' to is NOT the root, and falls out at the
1560
+ // existence check below.)
1561
+ const normalized = normalizePathForSandbox(pathPattern);
1562
+ const normalizedPath = normalized === '' ? '' : normalized.replace(/\/+$/, '') || '/';
1563
+ logForDebugging(`[Sandbox Linux] Processing write path: ${pathPattern} -> ${normalizedPath}`);
1564
+ // Skip /dev/* paths since --dev /dev already handles them
1565
+ if (normalizedPath.startsWith('/dev/')) {
1566
+ logForDebugging(`[Sandbox Linux] Skipping /dev path: ${normalizedPath}`);
1567
+ continue;
1568
+ }
1569
+ if (!fs.existsSync(normalizedPath)) {
1570
+ logForDebugging(`[Sandbox Linux] Skipping non-existent write path: ${normalizedPath}`);
1571
+ continue;
1572
+ }
1573
+ // Check if path is a symlink pointing outside expected boundaries
1574
+ // bwrap follows symlinks, so --bind on a symlink makes the target writable
1575
+ // This could unexpectedly expose paths the user didn't intend to allow
1576
+ try {
1577
+ const resolvedPath = fs.realpathSync(normalizedPath);
1578
+ // Trim trailing slashes before comparing: realpathSync never returns
1579
+ // a trailing slash, but normalizedPath may have one, which would cause
1580
+ // a false mismatch and incorrectly treat the path as a symlink.
1581
+ const normalizedForComparison = normalizedPath.replace(/\/+$/, '');
1582
+ if (resolvedPath !== normalizedForComparison &&
1583
+ isSymlinkOutsideBoundary(normalizedPath, resolvedPath)) {
1584
+ logForDebugging(`[Sandbox Linux] Skipping symlink write path pointing outside expected location: ${pathPattern} -> ${resolvedPath}`);
1585
+ continue;
1586
+ }
1587
+ }
1588
+ catch {
1589
+ // realpathSync failed - path might not exist or be accessible, skip it
1590
+ logForDebugging(`[Sandbox Linux] Skipping write path that could not be resolved: ${normalizedPath}`);
1591
+ continue;
1592
+ }
1593
+ args.push('--bind', normalizedPath, normalizedPath);
1594
+ allowedWritePaths.push(normalizedPath);
1595
+ }
1596
+ ancestorPinInsertAt = isAllowedWriteRoot('/')
1597
+ ? args.length
1598
+ : beneathAllowBinds;
1599
+ let stubSkipVetoInputs;
1600
+ const getStubSkipVetoInputs = () => {
1601
+ if (stubSkipVetoInputs !== undefined) {
1602
+ return stubSkipVetoInputs;
1603
+ }
1604
+ const unusable = (cause) => {
1605
+ logForDebugging(`[Sandbox Linux] Read-deny prediction unusable (${cause}); keeping every deny placeholder, which refuses to start wherever a covering deny directory is read-only: bubblewrap cannot create a placeholder's mount point there`, { level: 'warn' });
1606
+ return { usable: false };
1607
+ };
1608
+ // Single-shot: readDenyEntries() owns the retry for the one thing that
1609
+ // throws here, its listing of '/'. A guess would survive a retry
1610
+ // anyway, being a settled verdict about a path that cannot be looked at.
1611
+ try {
1612
+ const allowedWritePathsBothForms = allowedWritePaths.flatMap(mountForms);
1613
+ const prospectiveReadDenyTmpfsDirsBothForms = readDenyPlan().flatMap(({ mount }) => {
1614
+ if (!mount?.isDirectory)
1615
+ return [];
1616
+ // The tmpfs hides its landing, and the spelling the entry
1617
+ // resolved to when that differs. '/' is never a landing, and
1618
+ // must not enter the prediction as a spelling either (a link
1619
+ // to the root resolves there, and its holder is the landing):
1620
+ // every covering directory lies under '/', so predicting a
1621
+ // tmpfs there would veto every skip on the whole host.
1622
+ return [
1623
+ ...new Set([...mountForms(mount.named), mount.landing]),
1624
+ ].filter(form => form !== '/');
1625
+ });
1626
+ // Every canonicalForm call site runs inside this derivation or after
1627
+ // the deny loop that triggers it, so a guess recorded by now was made
1628
+ // about the derivation's own inputs. A future caller that resolves
1629
+ // earlier can only add guesses here, which vetoes more skips, never
1630
+ // fewer.
1631
+ stubSkipVetoInputs =
1632
+ canonicalFormGuesses.size > 0
1633
+ ? unusable(`no canonical location for ${[...canonicalFormGuesses].join(', ')}`)
1634
+ : {
1635
+ usable: true,
1636
+ allowedWritePathsBothForms,
1637
+ prospectiveReadDenyTmpfsDirsBothForms,
1638
+ };
1639
+ }
1640
+ catch (err) {
1641
+ stubSkipVetoInputs = unusable(`deriving it threw: ${err}`);
1642
+ }
1643
+ return stubSkipVetoInputs;
1644
+ };
1645
+ // Deny writes within allowed paths (user-specified + mandatory denies)
1646
+ const denyPaths = [
1647
+ ...(writeConfig.denyWithinAllow || []),
1648
+ ...(await linuxGetMandatoryDenyPaths(ripgrepConfig, mandatoryDenySearchDepth, allowGitConfig, abortSignal)),
1649
+ ];
1650
+ // Duplicate deny entries must be collapsed: a duplicate
1651
+ // --ro-bind /dev/null <dest> hits a char device on the second pass and
1652
+ // bwrap's ensure_file() falls through to creat() on a read-only mount.
1653
+ const seenDenyWrite = new Set();
1654
+ // Placeholder destination -> the index of its source in denyWriteArgs, so
1655
+ // a later deny reaching the same destination can upgrade a /dev/null
1656
+ // placeholder to the directory form in place (see the emission below).
1657
+ const placeholderSourceArgIndex = new Map();
1658
+ // PRE-PASS (order-independent): record the directories the loop below
1659
+ // re-binds read-only, BEFORE any stub decision, so the read-only
1660
+ // conclusion does not depend on where an enclosing directory appears in
1661
+ // the caller's denyWrite ordering. It applies the loop's own resolution,
1662
+ // the same symlink re-check, and the same isWithinAnyAllowedWritePath
1663
+ // gate as the --ro-bind emission, and it records every raw spelling each
1664
+ // directory is reached through. A recorded directory is EVIDENCE for
1665
+ // skipping a stub only if it also passes the guard's vetoes below (no
1666
+ // read-deny tmpfs contains it in any spelling; no allowed write path is
1667
+ // both beneath it and under such a tmpfs), which exclude every way its
1668
+ // subtree could be writable in the sandbox. Keep the two passes in lockstep: a
1669
+ // directory recorded here is either re-bound read-only by the loop or
1670
+ // skipped because a recorded directory above it survived the vetoes and
1671
+ // is bound in its place, so every record still stands for a bind that
1672
+ // lands — unless a symlink appears in its path between the two passes,
1673
+ // where the loop masks that component and emits no bind for the
1674
+ // directory (the re-check below); an emitted one missing from the record
1675
+ // only costs a spurious abort.
1676
+ for (const pathPattern of denyPaths) {
1677
+ const rawPath = normalizePathForSandbox(pathPattern);
1678
+ if (rawPath.startsWith('/dev/')) {
1679
+ continue;
1680
+ }
1681
+ const resolvedPath = resolveSymlinkedDenyPath(rawPath);
1682
+ if (resolvedPath === null || resolvedPath.startsWith('/dev/')) {
1683
+ continue;
1684
+ }
1685
+ // Same defense-in-depth re-check as the loop: there the --ro-bind is
1686
+ // replaced by a symlink mask, so such a directory must not be recorded
1687
+ // as re-bound here.
1688
+ if (findSymlinkInPath(resolvedPath, allowedWritePaths)) {
1689
+ continue;
1690
+ }
1691
+ let isDirectory = false;
1692
+ try {
1693
+ isDirectory = fs.statSync(resolvedPath).isDirectory();
1694
+ }
1695
+ catch {
1696
+ continue; // absent (or vanished): not a read-only re-bound directory
1697
+ }
1698
+ if (!isDirectory) {
1699
+ continue;
1700
+ }
1701
+ if (isWithinAnyAllowedWritePath(resolvedPath)) {
1702
+ // Keep every spelling this dest is reached through (the
1703
+ // re-application passes record the first-seen raw spelling beside
1704
+ // the dest, which this pass cannot assume): the guard below treats
1705
+ // the directory as unsafe if a read-deny tmpfs contains ANY of them.
1706
+ let spellings = readOnlyDenyDirSpellings.get(resolvedPath);
1707
+ if (spellings === undefined) {
1708
+ spellings = new Set();
1709
+ readOnlyDenyDirSpellings.set(resolvedPath, spellings);
1710
+ }
1711
+ spellings.add(rawPath);
1712
+ }
1713
+ }
1714
+ // Per-covering-dir veto verdict, computed once per recorded directory
1715
+ // (the inputs never change during the deny loop) instead of per absent
1716
+ // deny entry.
1717
+ // INVARIANT: a stub, or an existing deny path's own bind, is skipped
1718
+ // only under a recorded covering deny directory that was judged against
1719
+ // a prediction that could be derived, that no read-deny tmpfs contains
1720
+ // (it, or any spelling it was reached through), and beneath which no
1721
+ // allowed write path sits that a read-deny tmpfs also covers. '/' is the
1722
+ // exception: it contains every allowed write path, so
1723
+ // coveredBySafeReadOnlyDenyDir judges a vetoed '/' against the candidate
1724
+ // instead — see the branch there.
1725
+ // The skip rests on the emission order: nothing host-backed and writable
1726
+ // lands after the buffered read-only binds, so a covering bind is the
1727
+ // last word on its subtree unless a tmpfs above it drops that bind at
1728
+ // emission. Where a veto could apply, keep the stub: the pre-existing
1729
+ // abort is preferable to a silently creatable deny path.
1730
+ const coveringDirUnsafeVerdicts = new Map();
1731
+ const coveringDirIsUnsafe = (denyDir) => {
1732
+ const cached = coveringDirUnsafeVerdicts.get(denyDir);
1733
+ if (cached !== undefined) {
1734
+ return cached;
1735
+ }
1736
+ const vetoInputs = getStubSkipVetoInputs();
1737
+ const unsafe =
1738
+ // (0) the prediction of what the denyRead loop will mount is
1739
+ // unusable, so no veto below can be trusted to fire. Veto
1740
+ // everything rather than nothing: a prediction that failed is no
1741
+ // evidence that this directory is reliably read-only.
1742
+ !vetoInputs.usable ||
1743
+ // (i) an allowed write path both strictly beneath the dir and at or
1744
+ // under a prospective read-deny tmpfs. Re-applying that tmpfs
1745
+ // after the dir's read-only bind is the one pass that mounts
1746
+ // anything inside such a path at all, and it restores read-only,
1747
+ // so this names no live re-opening route today and no
1748
+ // configuration is known that needs it. It is the only veto that
1749
+ // speaks about a writable path under the dir: should a pass after
1750
+ // the deny binds ever restore one writable again, the skip fails
1751
+ // closed here.
1752
+ vetoInputs.allowedWritePathsBothForms.some(writePath => isStrictlyUnder(writePath, denyDir) &&
1753
+ vetoInputs.prospectiveReadDenyTmpfsDirsBothForms.some(tmpfsDir => isAtOrUnder(writePath, tmpfsDir))) ||
1754
+ // (ii) a read-deny tmpfs CONTAINING the dir: its own --ro-bind is
1755
+ // dropped as hidden-by-the-tmpfs at emission, and a tmpfs above
1756
+ // it restores allowed paths around it — either way the dir's own
1757
+ // bind is not the last word on that subtree. A tmpfs containing
1758
+ // only a raw spelling it was reached through counts too; that
1759
+ // over-predicts, which only keeps a stub.
1760
+ vetoInputs.prospectiveReadDenyTmpfsDirsBothForms.some(tmpfsDir => [denyDir, ...(readOnlyDenyDirSpellings.get(denyDir) ?? [])].some(spelling => isAtOrUnder(spelling, tmpfsDir)));
1761
+ coveringDirUnsafeVerdicts.set(denyDir, unsafe);
1762
+ return unsafe;
1763
+ };
1764
+ // Materialized once: the pre-pass above fully populates the map and the
1765
+ // deny loop never mutates it.
1766
+ const readOnlyDenyDirs = [...readOnlyDenyDirSpellings.keys()];
1767
+ // Is `candidate` already unwritable in the sandbox: strictly under a
1768
+ // recorded read-only deny directory that survives every
1769
+ // coveringDirIsUnsafe veto? Strictly, because a deny equal to a recorded
1770
+ // directory IS that covering bind and must be emitted (an absent path
1771
+ // never equals one). Stops at the first vetoed covering directory.
1772
+ const coveredBySafeReadOnlyDenyDir = (candidate) => {
1773
+ let covered = false;
1774
+ for (const denyDir of readOnlyDenyDirs) {
1775
+ if (!isStrictlyUnder(candidate, denyDir))
1776
+ continue;
1777
+ if (coveringDirIsUnsafe(denyDir)) {
1778
+ if (denyDir === '/') {
1779
+ // A vetoed '/' does not disqualify an inner recorded directory:
1780
+ // everything lies beneath it, so it would veto every skip and
1781
+ // stub each absent mandatory-deny path of a write-denied cwd
1782
+ // after that cwd's own bind — the startup abort. Its descendants
1783
+ // are decided by their own recorded directories.
1784
+ //
1785
+ // It does still COVER a candidate outside every predicted
1786
+ // read-deny tmpfs. Its own --ro-bind / / holds the whole tree
1787
+ // read-only from where it is emitted, burying the allow loop's
1788
+ // binds, and the only writable surface after it is a re-applied
1789
+ // tmpfs itself: the re-application passes no allowed write
1790
+ // paths, so everything it restores comes back read-only. An
1791
+ // allowed write path beneath the root is therefore not a vector
1792
+ // here, and vetoing on one only brings the abort back for
1793
+ // `allowOnly: ['/', <dir>]`. An underivable prediction proves
1794
+ // nothing and keeps the stub. Without this branch, `allowOnly:
1795
+ // ['/']` with `denyWithinAllow: ['/']` stubs each absent cwd
1796
+ // dotfile on the read-only root it just mounted — the startup
1797
+ // abort — whenever anything vetoes '/', which an unusable
1798
+ // prediction or an allowed write path under a read-deny tmpfs
1799
+ // does. The branch is strictly stricter than covering on the
1800
+ // root's own bind, and it is what decides those two cases.
1801
+ const vetoInputs = getStubSkipVetoInputs();
1802
+ if (vetoInputs.usable &&
1803
+ !vetoInputs.prospectiveReadDenyTmpfsDirsBothForms.some(tmpfsDir => isAtOrUnder(candidate, tmpfsDir))) {
1804
+ covered = true;
1805
+ }
1806
+ continue;
1807
+ }
1808
+ return false;
1809
+ }
1810
+ covered = true;
1811
+ }
1812
+ return covered;
1813
+ };
1814
+ for (const pathPattern of denyPaths) {
1815
+ const rawPath = normalizePathForSandbox(pathPattern);
1816
+ // Skip /dev/* paths since --dev /dev already handles them
1817
+ if (rawPath.startsWith('/dev/')) {
1818
+ continue;
1819
+ }
1820
+ // Resolve-before-mask: normalizePathForSandbox keeps the raw symlink
1821
+ // path whenever resolution crosses isSymlinkOutsideBoundary (the
1822
+ // common dotfiles case), so canonicalize here. Every mask/bind below
1823
+ // must be computed against the resolved path — bwrap dest-resolves
1824
+ // symlinks, so a mask on the raw path either aborts startup (dir
1825
+ // symlink) or lands on the target inode anyway (file symlink). Writes
1826
+ // through the original symlinked path resolve to the same denied
1827
+ // target inside the mount namespace.
1828
+ const normalizedPath = resolveSymlinkedDenyPath(rawPath);
1829
+ if (normalizedPath === null) {
1830
+ // Unresolvable: a symlink cycle, or a chain past the ELOOP bound.
1831
+ // Fail closed. Dropping the deny here would sandbox the command with
1832
+ // the path unprotected, so instead mask the symlink component and let
1833
+ // bwrap refuse to start. When no component is a symlink inside an
1834
+ // allowed write path there is nothing to protect: the path is already
1835
+ // read-only from the initial --ro-bind / /.
1836
+ const unresolvableSymlink = findSymlinkInPath(rawPath, allowedWritePaths);
1837
+ if (unresolvableSymlink && !seenDenyWrite.has(unresolvableSymlink)) {
1838
+ seenDenyWrite.add(unresolvableSymlink);
1839
+ denyWriteArgs.push('--ro-bind', '/dev/null', unresolvableSymlink);
1840
+ denyWriteRawDests.set(unresolvableSymlink, rawPath);
1841
+ }
1842
+ logForDebugging(`[Sandbox Linux] Deny path could not be resolved through symlinks, failing closed: ${rawPath}`);
1843
+ continue;
1844
+ }
1845
+ if (normalizedPath !== rawPath) {
1846
+ logForDebugging(`[Sandbox Linux] Resolved symlinked deny path: ${rawPath} -> ${normalizedPath}`);
1847
+ }
1848
+ // Re-check after resolution: a deny path can only now land in /dev (e.g.
1849
+ // a symlink into it), where --dev /dev has already replaced the tree and
1850
+ // a bind would either miss or fight the new devtmpfs.
1851
+ if (normalizedPath.startsWith('/dev/')) {
1852
+ continue;
1853
+ }
1854
+ // Dedup post-resolution: distinct spellings (tilde vs absolute, via
1855
+ // symlink vs direct) converge to the same real path here.
1856
+ if (seenDenyWrite.has(normalizedPath))
1857
+ continue;
1858
+ seenDenyWrite.add(normalizedPath);
1859
+ // Defense-in-depth: the resolved path should be symlink-free for its
1860
+ // existing prefix, but the tree may change between resolution and this
1861
+ // check. A hit still fails closed — bwrap refuses to start rather than
1862
+ // sandboxing with an unprotected deny path.
1863
+ const symlinkInPath = findSymlinkInPath(normalizedPath, allowedWritePaths);
1864
+ if (symlinkInPath) {
1865
+ if (!seenDenyWrite.has(symlinkInPath)) {
1866
+ seenDenyWrite.add(symlinkInPath);
1867
+ denyWriteArgs.push('--ro-bind', '/dev/null', symlinkInPath);
1868
+ denyWriteRawDests.set(symlinkInPath, rawPath);
1869
+ }
1870
+ logForDebugging(`[Sandbox Linux] Mounted /dev/null at symlink ${symlinkInPath} to prevent symlink replacement attack`);
1871
+ continue;
1872
+ }
1873
+ // A mount point an earlier sandbox left on the host (see
1874
+ // isStaleBwrapMountPoint) is an absent deny path in all but name. Bound
1875
+ // onto itself as an existing file it would never be tracked, so never
1876
+ // removed, and on the host its existence can be the whole meaning (a
1877
+ // lockfile's). Cover it with /dev/null like the absent leaf below and
1878
+ // track it, so cleanupBwrapMountPoints() takes it away. Same gate as
1879
+ // that branch. Under a read-only denied directory nothing needs
1880
+ // covering (the file is already unwritable there), but it is still
1881
+ // tracked: it is no more the caller's file for being there.
1882
+ if ((isWithinAnyAllowedWritePath(path.dirname(normalizedPath)) ||
1883
+ isWithinAnyAllowedWritePath(normalizedPath)) &&
1884
+ isStaleBwrapMountPoint(normalizedPath)) {
1885
+ if (!coveredBySafeReadOnlyDenyDir(normalizedPath)) {
1886
+ denyWriteArgs.push('--ro-bind', '/dev/null', normalizedPath);
1887
+ denyWriteRawDests.set(normalizedPath, rawPath);
1888
+ }
1889
+ bwrapMountPoints.add(normalizedPath);
1890
+ registerExitCleanupHandler();
1891
+ logForDebugging(`[Sandbox Linux] Re-covering a mount point an earlier sandbox left behind: ${normalizedPath}`);
1892
+ continue;
1893
+ }
1894
+ // Handle non-existent paths by mounting /dev/null to block creation.
1895
+ // Without this, a sandboxed process could mkdir+write a denied path that
1896
+ // doesn't exist yet, bypassing the deny rule entirely.
1897
+ //
1898
+ // bwrap creates empty files on the host as mount points for these binds.
1899
+ // We track them in bwrapMountPoints so cleanupBwrapMountPoints() can
1900
+ // remove them after the command exits.
1901
+ if (!fs.existsSync(normalizedPath)) {
1902
+ // Fix 1 (worktree): If any existing component above the deny path is
1903
+ // a file (not a directory), skip the deny entirely. You can't mkdir
1904
+ // under a file, so the deny path can never be created. This handles
1905
+ // git worktrees where .git is a file. Asked of what lies above the
1906
+ // path, not of the path: another sandbox's bubblewrap may have made a
1907
+ // mount point at it since the look above, and a file there is no
1908
+ // reason to leave it unbound.
1909
+ if (hasFileAncestor(path.dirname(normalizedPath))) {
1910
+ logForDebugging(`[Sandbox Linux] Skipping deny path with file ancestor (cannot create paths under a file): ${normalizedPath}`);
1911
+ continue;
1912
+ }
1913
+ // Find the deepest existing ancestor directory
1914
+ let ancestorPath = path.dirname(normalizedPath);
1915
+ while (ancestorPath !== '/' && !fs.existsSync(ancestorPath)) {
1916
+ ancestorPath = path.dirname(ancestorPath);
1917
+ }
1918
+ // Only protect if the existing ancestor is within an allowed write path.
1919
+ // If not, the path is already read-only from --ro-bind / /.
1920
+ // Same predicate as the pre-pass and the --ro-bind gate (equality on
1921
+ // the absent normalizedPath itself is unreachable — allow entries
1922
+ // exist, the deny path does not).
1923
+ const ancestorIsWithinAllowedPath = isWithinAnyAllowedWritePath(ancestorPath) ||
1924
+ isWithinAnyAllowedWritePath(normalizedPath);
1925
+ // An ancestor inside a directory that an earlier deny re-bound
1926
+ // read-only (e.g. an explicit denyWrite on the project dir) is
1927
+ // already read-only in the sandbox: the deny path cannot be created
1928
+ // there, and stubbing it would make bwrap creat() a mount point
1929
+ // inside that read-only mount and abort. The order-independent
1930
+ // pre-pass above has already recorded every directory the loop
1931
+ // re-binds read-only, so a covering directory deny is visible here
1932
+ // regardless of where it appears in denyPaths. A recorded covering
1933
+ // directory is evidence for skipping only if it survives the
1934
+ // coveringDirIsUnsafe vetoes (see the INVARIANT at its definition).
1935
+ // (Tested on the absent path itself: a recorded directory that
1936
+ // covers it is at-or-above its deepest existing ancestor, since
1937
+ // recorded directories exist.)
1938
+ const ancestorIsWithinReadOnlyDeny = coveredBySafeReadOnlyDenyDir(normalizedPath);
1939
+ if (ancestorIsWithinAllowedPath && !ancestorIsWithinReadOnlyDeny) {
1940
+ const firstNonExistent = findFirstNonExistentComponent(normalizedPath);
1941
+ // Fix 2: If firstNonExistent is an intermediate component (not the
1942
+ // leaf deny path itself), mount a read-only empty directory instead
1943
+ // of /dev/null. This prevents the component from appearing as a file
1944
+ // which breaks tools that expect to traverse it as a directory.
1945
+ const isIntermediate = firstNonExistent !== normalizedPath;
1946
+ const source = isIntermediate
1947
+ ? (emptySource ?? (emptySource = ensureEmptyMountSourceDir()))
1948
+ : '/dev/null';
1949
+ // One mount point per destination. Deny paths are deduplicated on
1950
+ // the deny path, but a placeholder lands on the first MISSING
1951
+ // component, so two denies sharing one arrive here with a single
1952
+ // destination — denyWrite '<cwd>/.claude' together with the
1953
+ // mandatory '<cwd>/.claude/commands', in a project with no
1954
+ // `.claude/`. Two binds there make bwrap refuse to start when they
1955
+ // disagree about the destination's kind ("Can't mkdir <dest>: Not a
1956
+ // directory"). The directory form wins the disagreement: an empty
1957
+ // read-only directory blocks creating the destination and everything
1958
+ // below it exactly as /dev/null does, and stays traversable for the
1959
+ // deeper deny that asked for a directory.
1960
+ const placeholderAt = placeholderSourceArgIndex.get(firstNonExistent);
1961
+ if (placeholderAt !== undefined) {
1962
+ if (isIntermediate)
1963
+ denyWriteArgs[placeholderAt] = source;
1964
+ logForDebugging(`[Sandbox Linux] Reusing the mount point at ${firstNonExistent} to block creation of ${normalizedPath}`);
1965
+ continue;
1966
+ }
1967
+ denyWriteArgs.push('--ro-bind', source, firstNonExistent);
1968
+ placeholderSourceArgIndex.set(firstNonExistent, denyWriteArgs.length - 2);
1969
+ // First writer wins for a destination several denies share (the
1970
+ // reuse branch above returns before reaching this), and the record
1971
+ // is purely additive: it only gives the tmpfs and mask comparisons
1972
+ // below a second spelling to test, and `dest` itself is always
1973
+ // tested.
1974
+ denyWriteRawDests.set(firstNonExistent, rawPath);
1975
+ bwrapMountPoints.add(firstNonExistent);
1976
+ registerExitCleanupHandler();
1977
+ logForDebugging(`[Sandbox Linux] Mounted ${isIntermediate ? 'empty dir' : '/dev/null'} at ${firstNonExistent} to block creation of ${normalizedPath}`);
1978
+ }
1979
+ else if (ancestorIsWithinReadOnlyDeny) {
1980
+ logForDebugging(`[Sandbox Linux] Skipping non-existent deny path inside a read-only denied directory (already uncreatable): ${normalizedPath}`);
1981
+ }
1982
+ else {
1983
+ logForDebugging(`[Sandbox Linux] Skipping non-existent deny path not within allowed paths: ${normalizedPath}`);
1984
+ }
1985
+ continue;
1986
+ }
1987
+ // Only add deny binding if this path is within an allowed write path
1988
+ // Otherwise it's already read-only from the initial --ro-bind / /
1989
+ const isWithinAllowedPath = isWithinAnyAllowedWritePath(normalizedPath);
1990
+ if (isWithinAllowedPath) {
1991
+ // Already unwritable under a read-only denied directory (the
1992
+ // existing-path twin of the stub skip above). Veto (ii) keeps the
1993
+ // covering bind through the emission filter; a symlinked spelling
1994
+ // keeps its own bind because the re-application passes below key
1995
+ // off emitted raw spellings.
1996
+ if (rawPath === normalizedPath &&
1997
+ coveredBySafeReadOnlyDenyDir(normalizedPath)) {
1998
+ logForDebugging(`[Sandbox Linux] Skipping deny path already under read-only denied directory: ${normalizedPath}`);
1999
+ continue;
2000
+ }
2001
+ // A deny's read-only bind is emitted after every allow bind, so an
2002
+ // allowed write path beneath it comes back read-only instead of
2003
+ // stopping the sandbox from starting. Say so: the config asked for
2004
+ // both and only the deny takes effect.
2005
+ for (const buried of allowedWritePaths) {
2006
+ if (isStrictlyUnder(buried, normalizedPath)) {
2007
+ logForDebugging(`[Sandbox Linux] Write deny ${normalizedPath} covers allowed write path ${buried}; ${buried} will be read-only`, { level: 'warn' });
2008
+ }
2009
+ }
2010
+ denyWriteArgs.push('--ro-bind', normalizedPath, normalizedPath);
2011
+ denyWriteRawDests.set(normalizedPath, rawPath);
2012
+ }
2013
+ else {
2014
+ logForDebugging(`[Sandbox Linux] Skipping deny path not within allowed paths: ${normalizedPath}`);
2015
+ }
2016
+ }
2017
+ }
2018
+ else {
2019
+ // No write restrictions: Allow all writes
2020
+ args.push('--bind', '/', '/');
2021
+ // Recording '/' makes isWithinAnyAllowedWritePath and isAllowedWriteRoot
2022
+ // say so, which is the '/'-write-root shape ancestorPinArgs treats
2023
+ // specially. Nothing is ever restored for it: no read-deny tmpfs lands
2024
+ // at '/'.
2025
+ allowedWritePaths.push('/');
2026
+ ancestorPinInsertAt = args.length;
2027
+ }
2028
+ // denyWriteArgs is emitted after the denyRead loop below.
2029
+ // Handle read restrictions by mounting tmpfs over denied paths.
2030
+ // Non-glob spellings arrive slash-free from normalizePathForSandbox — the
2031
+ // comparisons below (hidden-by-tmpfs prefixes, the exact-match allowRead
2032
+ // skip, pushReadDenyDirMounts' re-bind checks) depend on that.
2033
+ // The read-deny mounts emitted so far, in emission order: directories
2034
+ // masked by --tmpfs with what their restore loops re-bound, and files
2035
+ // masked by --ro-bind <source> <dest> (/dev/null for read-deny, the
2036
+ // sentinel fake for a credential mask). `landing` is where each mount sits
2037
+ // in the sandbox. Every later decision reads these records: the ancestor
2038
+ // pins, the deny-bind emission filter and the re-application passes.
2039
+ const readDenyTmpfsUnits = [];
2040
+ const fileMasks = [];
2041
+ // Replay the tmpfs units in order (bwrap is last-mount-wins): is `location`
2042
+ // beneath a unit's tmpfs and not brought back by a restore since? A restore
2043
+ // counts only where it lands. One reached through a symlink puts the
2044
+ // target's inode at the NAME; the target's own path is still under the
2045
+ // tmpfs, and asking there must not answer "brought back".
2046
+ const isHiddenByTmpfs = (location, broughtBackBy) => {
2047
+ const broughtBack = (restored) => restored.some(r => isAtOrUnder(location, r.dest));
2048
+ let hidden = false;
2049
+ for (const unit of readDenyTmpfsUnits) {
2050
+ if (isAtOrUnder(location, unit.landing)) {
2051
+ hidden = true;
2052
+ }
2053
+ if (broughtBack(unit.restoredWrites) ||
2054
+ (broughtBackBy === 'writes and reads' &&
2055
+ broughtBack(unit.restoredReads))) {
2056
+ hidden = false;
2057
+ }
2058
+ }
2059
+ return hidden;
2060
+ };
2061
+ // Entries a glob expansion produced for a directory it could not list.
2062
+ // What the pattern matches beneath an allowed path in there was never
2063
+ // found, so binding that path back over the tmpfs would show every one of
2064
+ // those matches unmasked.
2065
+ const unlistableDenyDirs = new Set(readConfig?.unlistableDenyDirs ?? []);
2066
+ // Every location the read section hides, each one where its mount lands:
2067
+ // one per entry that mounts something — a directory's tmpfs, the stand-in
2068
+ // tmpfs of an entry that could not be inspected or that resolves to '/', a
2069
+ // file's /dev/null mask — and one per masked credential file. An entry that
2070
+ // mounts nothing hides nothing, and must not cost a carve-out; a stand-in
2071
+ // hides where it lands, which is above the entry that asked for it. A
2072
+ // landing the loop below then skips as already hidden stays in this list:
2073
+ // it refuses a carve-out the mounts alone would have allowed, which is the
2074
+ // safe direction and one comparison fewer.
2075
+ const readDeniedLocations = [
2076
+ ...readDenyPlan().flatMap(({ mount, liftedFile }) => mount === undefined || liftedFile ? [] : [mount.landing]),
2077
+ ...(maskedFileBinds ?? []).map(mask => canonicalForm(mask.realPath)),
2078
+ ];
2079
+ // What the read section hides at, inside, or around `target`, ignoring the
2080
+ // tmpfs landing at `landing` and every deny above it — those are what a
2081
+ // carve-out restored into that tmpfs is the exception to. Everything else
2082
+ // that overlaps the target wins over the carve-out. Both sides are resolved
2083
+ // locations: every read deny mounts where its entry resolves, and `target`
2084
+ // is what the carve-out resolves to, so no spelling has to be guessed at.
2085
+ // Returns the offending location, for the debug line.
2086
+ const readDenialAround = (target, landing) => readDeniedLocations.find(denied => !isAtOrUnder(landing, denied) &&
2087
+ (isAtOrUnder(denied, target) || isAtOrUnder(target, denied)));
2088
+ for (const { normalizedPath, mount, liftedFile } of readDenyPlan()) {
2089
+ if (mount === undefined) {
2090
+ logForDebugging(`[Sandbox Linux] Read deny path mounts nothing this wrap can place (absent, or uninspectable, or a link to '/' with nothing but '/' holding it): ${normalizedPath}`);
2091
+ continue;
2092
+ }
2093
+ const { landing, isDirectory, isStandIn } = mount;
2094
+ // One mount per location. A tmpfs emitted so far already hides this
2095
+ // place, and nothing bound back over it shows the host there again, so a
2096
+ // mount here would be created inside that tmpfs and change nothing. This
2097
+ // also collapses two spellings of one directory, and the entries a
2098
+ // collapsed read-deny glob leaves beneath a directory it already denies.
2099
+ if (isHiddenByTmpfs(landing, 'writes and reads')) {
2100
+ logForDebugging(`[Sandbox Linux] Skipping read deny already hidden by a denyRead tmpfs: ${landing}`);
2101
+ continue;
2102
+ }
2103
+ if (isDirectory) {
2104
+ // A stand-in for an entry this wrap cannot vouch for hides everything
2105
+ // beneath it: nothing under it can be vouched for either. So does a
2106
+ // directory a glob expansion could not enumerate.
2107
+ if (isStandIn) {
2108
+ logForDebugging(`[Sandbox Linux] Read deny path ${normalizedPath} cannot be mounted where it names; hiding ${landing} instead`, { level: 'warn' });
2109
+ }
2110
+ const unlistable = unlistableDenyDirs.has(normalizedPath) ||
2111
+ unlistableDenyDirs.has(landing);
2112
+ if (unlistable) {
2113
+ logForDebugging(`[Sandbox Linux] Read-denied directory could not be listed when the glob was expanded; restoring nothing beneath it: ${landing}`, { level: 'warn' });
2114
+ }
2115
+ const restoresNothing = isStandIn || unlistable;
2116
+ const restored = pushReadDenyDirMounts(args, {
2117
+ landing,
2118
+ allowedWritePaths: restoresNothing ? [] : allowedWritePaths,
2119
+ readAllowPaths: restoresNothing ? [] : readAllowPaths(),
2120
+ resolve: canonicalLocationOf,
2121
+ readDenialAround: restoreTarget => readDenialAround(restoreTarget, landing),
2122
+ nameLocation: nameLocationOf,
2123
+ });
2124
+ readDenyTmpfsUnits.push({ landing, ...restored });
2125
+ }
2126
+ else {
2127
+ // For files, only an exact allowRead match overrides the deny: an
2128
+ // entry that names this very file, through whatever symlinked
2129
+ // directories. One that is a symlink to it names the link and lifts
2130
+ // nothing, or a link planted at an allowRead path would cancel the deny
2131
+ // of the file it points at. A directory allowRead does not un-deny a
2132
+ // file specifically listed in denyRead — otherwise denyRead: ['.env']
2133
+ // + allowRead: ['.'] silently drops the .env deny.
2134
+ if (liftedFile) {
2135
+ logForDebugging(`[Sandbox Linux] Skipping read deny for re-allowed path: ${normalizedPath}`);
2136
+ continue;
2137
+ }
2138
+ // For files, bind /dev/null instead of tmpfs, where the path resolves
2139
+ // like every other read-deny mount.
2140
+ args.push('--ro-bind', '/dev/null', landing);
2141
+ fileMasks.push({ source: '/dev/null', landing });
2142
+ }
2143
+ }
2144
+ // Whole-file credential masks: same bind shape as a file read-deny,
2145
+ // at the same place — where the path really resolves, which is what
2146
+ // covers a credential dotfile that is a symlink into a dotfile manager's
2147
+ // directory (~/.netrc, ~/.npmrc). realPath was already normalized
2148
+ // (tilde-expanded, realpath'd) by the caller. The fake's parent dir is
2149
+ // explicitly ro-bound at the end of this function, so the bind source is
2150
+ // never writable from inside the sandbox.
2151
+ for (const { realPath, fakePath } of maskedFileBinds ?? []) {
2152
+ const landing = canonicalForm(realPath);
2153
+ args.push('--ro-bind', fakePath, landing);
2154
+ fileMasks.push({ source: fakePath, landing });
2155
+ }
2156
+ // Ancestor pinning: the directories between a deny bind, file mask or
2157
+ // read-deny tmpfs and its covering allowed write root carry no mount, so
2158
+ // rename(2) on one moves the mount along with it and the path can be
2159
+ // recreated unprotected (mv .git aside; mkdir .git; write .git/hooks/x),
2160
+ // or, for a read-denied path, is simply not found by the next command's
2161
+ // wrap and left readable under its new name. ancestorPinArgs says what is
2162
+ // pinned and where. Seeds are landings because the walk takes dirname() of
2163
+ // each: a symlink spelling would pin the chain of the link, not of where
2164
+ // the mount sits. Deny binds, tmpfs units and masks are emitted later and
2165
+ // land on top of both pins and covers.
2166
+ const pinArgs = ancestorPinArgs([
2167
+ ...denyWriteRawDests.keys(),
2168
+ ...fileMasks.map(mask => mask.landing),
2169
+ ...readDenyTmpfsUnits.map(unit => unit.landing),
2170
+ ], {
2171
+ rootIsWriteRoot: isAllowedWriteRoot('/'),
2172
+ isWithinAllowedWrite: isWithinAnyAllowedWritePath,
2173
+ isAllowedWriteRoot,
2174
+ });
2175
+ args.splice(ancestorPinInsertAt, 0, ...pinArgs);
2176
+ // Emitting denyWrite last means these ro-binds layer on top of any write
2177
+ // paths the denyRead loop just re-bound. Before this ordering, tmpfs over
2178
+ // an ancestor of cwd would wipe the .git/hooks protection. But skip any
2179
+ // dest already masked by denyRead:
2180
+ //
2181
+ // - file masks: --ro-bind <host> <host> for denyWrite would undo
2182
+ // --ro-bind /dev/null <host> from denyRead, which landed first.
2183
+ // - tmpfs dirs: a dest at or under a denyRead tmpfs is already hidden, and
2184
+ // re-binding the host path on top of the tmpfs would expose the real
2185
+ // (read-denied) contents read-only. Writes inside the tmpfs never reach
2186
+ // the host, so the write-deny stays enforced without the bind. Exception:
2187
+ // if an allowed write path at-or-under that tmpfs covers the dest, the
2188
+ // denyRead loop re-bound it (the .git/hooks case) and the write-deny bind
2189
+ // is still required on top.
2190
+ // Both are decided by where the mask or tmpfs landed against the deny's
2191
+ // canonical dest, which is where its bind lands.
2192
+ const emittedDenyWriteDests = [];
2193
+ // Write paths already restored read-only by a dropped deny bind, so two
2194
+ // denies covering the same path emit one --ro-bind.
2195
+ const restoredReadOnlyWritePaths = new Set();
2196
+ for (let i = 0; i < denyWriteArgs.length; i += 3) {
2197
+ const dest = denyWriteArgs[i + 2];
2198
+ const rawDest = denyWriteRawDests.get(dest) ?? dest;
2199
+ // A mask's landing, not its dest: the landing is where the mask's bind
2200
+ // actually sits, and this deny's dest is canonical, so the two are
2201
+ // comparable as written.
2202
+ if (fileMasks.some(mask => mask.landing === dest))
2203
+ continue;
2204
+ if (isHiddenByTmpfs(dest, 'writes')) {
2205
+ logForDebugging(`[Sandbox Linux] Skipping denyWrite bind already hidden by denyRead tmpfs: ${dest}`);
2206
+ // The tmpfs hides this dest but not a write path one of the units
2207
+ // restored writable beneath it: that path is inside this write deny, so
2208
+ // restore it read-only, where the unit left it. Emitting the dest's own
2209
+ // bind instead would expose the read-denied directory around it.
2210
+ for (const unit of readDenyTmpfsUnits) {
2211
+ for (const writeMount of unit.restoredWrites) {
2212
+ const writePath = writeMount.dest;
2213
+ if (!isAtOrUnder(writePath, dest) &&
2214
+ !isAtOrUnder(writePath, rawDest)) {
2215
+ continue;
2216
+ }
2217
+ // A masked file needs no restore: its mask already holds it
2218
+ // unreadable and unwritable. A read-only bind of the real file here
2219
+ // would land ABOVE that mask, and the re-application below would
2220
+ // then have to put the mask back over a bind this loop only just
2221
+ // emitted — so skipping saves both that bind and its emitted-dest
2222
+ // entry. A masked file strictly BENEATH a restored directory is the
2223
+ // other case and stays: recording that directory is what puts its
2224
+ // mask back on top. Masks and restores are both keyed by where they
2225
+ // land, so one comparison answers for every spelling.
2226
+ if (fileMasks.some(mask => mask.landing === writePath)) {
2227
+ logForDebugging(`[Sandbox Linux] Leaving a masked file to its mask inside dropped denyWrite bind ${dest}: ${writePath}`);
2228
+ continue;
2229
+ }
2230
+ if (restoredReadOnlyWritePaths.has(writePath))
2231
+ continue;
2232
+ restoredReadOnlyWritePaths.add(writePath);
2233
+ args.push('--ro-bind', writeMount.source, writePath);
2234
+ // Like a deny bind, this one lands above whatever the read section
2235
+ // mounted inside the write path, so the re-application passes below
2236
+ // have to see it and put those mounts back.
2237
+ emittedDenyWriteDests.push(writePath);
2238
+ logForDebugging(`[Sandbox Linux] Restoring write path read-only inside dropped denyWrite bind ${dest}: ${writePath}`);
2239
+ }
2240
+ }
2241
+ continue;
2242
+ }
2243
+ args.push(denyWriteArgs[i], denyWriteArgs[i + 1], dest);
2244
+ emittedDenyWriteDests.push(dest);
2245
+ // The tmpfs / mask re-application passes below ask "does this bind sit
2246
+ // above a read-denied path?". A bind at the resolved dest also re-exposes
2247
+ // anything under the symlinked spelling, so record both.
2248
+ if (rawDest !== dest)
2249
+ emittedDenyWriteDests.push(rawDest);
2250
+ }
2251
+ // The inverse stacking problem: a denyWrite ro-bind whose dest strictly
2252
+ // contains a read-denied dir re-exposes that dir's real contents (the bind
2253
+ // landed after the tmpfs). Re-apply the tmpfs on top, with the allowRead
2254
+ // re-binds the denyRead loop emitted. A bind of '/' itself (allowOnly and
2255
+ // denyWithinAllow both naming it) contains every one of them, so
2256
+ // containment is root-aware. An allowed write path beneath such a tmpfs
2257
+ // lies inside the emitted deny dest too, so the deny wins: it comes back
2258
+ // read-only, visible as it was after the first pass but not writable.
2259
+ // Deeper units and masks under the same dest follow in the same order as
2260
+ // the first pass, so nothing restored here stays on top of one.
2261
+ for (const unit of readDenyTmpfsUnits) {
2262
+ const reExposingDest = emittedDenyWriteDests.find(dest => isStrictlyUnder(unit.landing, dest));
2263
+ if (reExposingDest !== undefined) {
2264
+ logForDebugging(`[Sandbox Linux] Re-applying denyRead tmpfs re-exposed by denyWrite bind: ${unit.landing}`);
2265
+ // Name the paths that lose their write access here, and to which deny.
2266
+ if (unit.restoredWrites.length > 0) {
2267
+ logForDebugging(`[Sandbox Linux] Restoring write paths read-only inside denyWrite bind ${reExposingDest}: ${unit.restoredWrites.map(w => w.dest).join(', ')}`);
2268
+ }
2269
+ // The recorded pairs go back as they are: each restore returns to where
2270
+ // the first pass put it, from the source it vetted then. Write paths
2271
+ // come back read-only, being inside the deny that re-exposed the tmpfs.
2272
+ // The tmpfs goes back at the unit's landing, which is the destination
2273
+ // the first pass gave bwrap for it.
2274
+ args.push('--tmpfs', unit.landing);
2275
+ for (const restored of [...unit.restoredWrites, ...unit.restoredReads]) {
2276
+ args.push('--ro-bind', restored.source, restored.dest);
2277
+ }
2278
+ }
2279
+ }
2280
+ // Same problem for masked files: the mask landed before the denyWrite
2281
+ // ancestor bind, so the real file is back. Re-apply the mask with its
2282
+ // original source (/dev/null for read-deny, the fake for credential mask).
2283
+ for (const mask of fileMasks) {
2284
+ if (emittedDenyWriteDests.some(dest => isAtOrUnder(mask.landing, dest))) {
2285
+ logForDebugging(`[Sandbox Linux] Re-applying file mask re-exposed by denyWrite bind: ${mask.landing}`);
2286
+ args.push('--ro-bind', mask.source, mask.landing);
2287
+ }
2288
+ }
2289
+ // INVARIANT: the fake-file store directory must never be writable from
2290
+ // inside the sandbox. If it were, a sandboxed process could plant a
2291
+ // symlink at a fake path and a later host-side write() would follow it,
2292
+ // or replace a fake's content so the bind exposes attacker bytes. Emit
2293
+ // last so it overlays any earlier --bind that covers the store dir
2294
+ // (e.g. allowWrite: ['/tmp'] when the store is under os.tmpdir()).
2295
+ if (maskedFileStoreDir !== undefined) {
2296
+ args.push('--ro-bind', maskedFileStoreDir, maskedFileStoreDir);
2297
+ }
2298
+ // INVARIANT, for the same reason: the empty directory the placeholders above
2299
+ // bind from must never be writable from inside the sandbox. It lives under
2300
+ // the system temp dir, so a caller's allowWrite or a host $TMPDIR under a
2301
+ // default write path makes it writable — and a placeholder binds it onto a
2302
+ // DENIED destination, so a write to the source lands at the deny. Emit last,
2303
+ // like the store, to overlay any earlier --bind that covers it. A same-uid
2304
+ // process on the host can still swap the source between wrap and run, which
2305
+ // is outside what this library defends against.
2306
+ if (emptySource !== undefined) {
2307
+ args.push('--ro-bind', emptySource, emptySource);
2308
+ }
2309
+ return args;
2310
+ }
2311
+ /**
2312
+ * Wrap a command with sandbox restrictions on Linux
2313
+ *
2314
+ * UNIX SOCKET BLOCKING (APPLY-SECCOMP):
2315
+ * This implementation uses a custom apply-seccomp binary to block Unix domain socket
2316
+ * creation for user commands while allowing network infrastructure:
2317
+ *
2318
+ * The two stages are described in README.md, "Unix Socket Restrictions
2319
+ * (Linux)".
2320
+ *
2321
+ * This solves the conflict between:
2322
+ * - Security: Blocking arbitrary Unix socket creation in user commands
2323
+ * - Functionality: Network sandboxing requires socat to call socket(AF_UNIX, ...) for bridge connections
2324
+ *
2325
+ * The seccomp-bpf filter blocks socket(AF_UNIX, ...) syscalls, preventing:
2326
+ * - Creating new Unix domain socket file descriptors
2327
+ *
2328
+ * Security limitations:
2329
+ * - Does NOT block operations (bind, connect, sendto, etc.) on inherited Unix socket FDs
2330
+ * - Does NOT prevent passing Unix socket FDs via SCM_RIGHTS
2331
+ * - For most sandboxing use cases, blocking socket creation is sufficient
2332
+ *
2333
+ * The filter allows:
2334
+ * - All TCP/UDP sockets (AF_INET, AF_INET6) for normal network operations
2335
+ * - All other syscalls
2336
+ *
2337
+ * PLATFORM NOTE:
2338
+ * The allowUnixSockets configuration is not path-based on Linux (unlike macOS)
2339
+ * because seccomp-bpf cannot inspect user-space memory to read socket paths.
2340
+ *
2341
+ * Requirements for seccomp filtering:
2342
+ * - Pre-built apply-seccomp binaries are included for x64 and ARM64
2343
+ * - Pre-generated BPF filters are included for x64 and ARM64
2344
+ * - Other architectures are not currently supported (no apply-seccomp binary available)
2345
+ * - To use sandboxing without Unix socket blocking on unsupported architectures,
2346
+ * set allowAllUnixSockets: true in your configuration
2347
+ * Dependencies are checked by checkLinuxDependencies() before enabling the sandbox.
2348
+ *
2349
+ * CALLER OBLIGATION: the euid and capability decisions behind the returned
2350
+ * string are made here, in the process that builds it, so the string must be
2351
+ * run by a process with the same euid and the same capability bounding and
2352
+ * inheritable sets.
2353
+ */
2354
+ export async function wrapCommandWithSandboxLinux(params) {
2355
+ const { command, commandId, needsNetworkRestriction, httpSocketPath, socksSocketPath, httpProxyPort, socksProxyPort, proxyAuthToken, caCertPath, javaAgentJarPath, readConfig, writeConfig, unsetEnvVars, setEnvVars, maskedFileBinds, maskedFileStoreDir, enableWeakerNestedSandbox, allowAllUnixSockets, binShell, ripgrepConfig = { command: 'rg' }, mandatoryDenySearchDepth = DEFAULT_MANDATORY_DENY_SEARCH_DEPTH, allowGitConfig = false, gitSafeDirectories, seccompConfig, bwrapPath, socatPath, observeSocketPath, abortSignal, } = params;
2356
+ // Determine if we have restrictions to apply
2357
+ // Read: denyOnly pattern - empty array means no restrictions
2358
+ // Write: allowOnly pattern - undefined means no restrictions, any config means restrictions
2359
+ const hasReadRestrictions = (readConfig && readConfig.denyOnly.length > 0) ||
2360
+ (maskedFileBinds !== undefined && maskedFileBinds.length > 0);
2361
+ const hasWriteRestrictions = writeConfig !== undefined;
2362
+ const hasEnvRestrictions = (unsetEnvVars !== undefined && unsetEnvVars.length > 0) ||
2363
+ (setEnvVars !== undefined && Object.keys(setEnvVars).length > 0);
2364
+ const hasGitConfig = (gitSafeDirectories?.length ?? 0) > 0;
2365
+ // Check if we need any sandboxing
2366
+ if (!needsNetworkRestriction &&
2367
+ !hasReadRestrictions &&
2368
+ !hasWriteRestrictions &&
2369
+ !hasEnvRestrictions &&
2370
+ !hasGitConfig) {
2371
+ return command;
2372
+ }
2373
+ // Mark this sandbox invocation as active. cleanupBwrapMountPoints() will
2374
+ // defer file deletion until this (and every other concurrent) invocation
2375
+ // has been cleaned up. The matching decrement happens in
2376
+ // cleanupBwrapMountPoints(), which the caller must invoke after the
2377
+ // spawned command exits. If wrapping fails below, the catch block
2378
+ // decrements so the count does not leak.
2379
+ activeSandboxCount++;
2380
+ // One encoded key for both carriers below (SRT_ENCODED_CMD for the seccomp
2381
+ // observer, the proxy username for network denies), so a violation seen
2382
+ // through either resolves to the same registry entry. macOS derives its log
2383
+ // tag from a single key the same way.
2384
+ const attributionKey = encodeSandboxedCommand(attributionKeyFor(command, commandId));
2385
+ const bwrapArgs = ['--new-session', '--die-with-parent'];
2386
+ let applySeccompPrefix;
2387
+ try {
2388
+ // ========== SECCOMP FILTER (Unix Socket Blocking) ==========
2389
+ // apply-seccomp wraps the workload and applies the baked-in BPF filter
2390
+ // that blocks socket(AF_UNIX, ...). Skipped when allowAllUnixSockets is true.
2391
+ if (!allowAllUnixSockets) {
2392
+ applySeccompPrefix = resolveApplySeccompPrefix(seccompConfig?.applyPath, seccompConfig?.argv0);
2393
+ if (!applySeccompPrefix) {
2394
+ logForDebugging('[Sandbox Linux] apply-seccomp binary not available - unix socket blocking disabled. ' +
2395
+ 'Install @anthropic-ai/sandbox-runtime globally for full protection.', { level: 'warn' });
2396
+ }
2397
+ else {
2398
+ logForDebugging('[Sandbox Linux] Applying seccomp filter for Unix socket blocking');
2399
+ }
2400
+ }
2401
+ else {
2402
+ logForDebugging('[Sandbox Linux] Skipping seccomp filter - allowAllUnixSockets is enabled');
2403
+ }
2404
+ // ========== VIOLATION OBSERVATION (best-effort) ==========
2405
+ // Only meaningful when apply-seccomp will run — it is the binary that
2406
+ // installs the USER_NOTIF filter and ships the listener fd.
2407
+ if (observeSocketPath && applySeccompPrefix) {
2408
+ if (fs.existsSync(observeSocketPath)) {
2409
+ bwrapArgs.push('--bind', observeSocketPath, observeSocketPath);
2410
+ bwrapArgs.push('--setenv', 'SRT_OBSERVE_SOCK', observeSocketPath);
2411
+ // Tag events with the encoded command so the violation store can
2412
+ // associate them with this invocation (parity with macOS log tag).
2413
+ bwrapArgs.push('--setenv', 'SRT_ENCODED_CMD', attributionKey);
2414
+ }
2415
+ else {
2416
+ logForDebugging('[Sandbox Linux] observe socket missing — supervisor not running; ' +
2417
+ 'continuing without violation monitoring');
2418
+ }
2419
+ }
2420
+ // ========== ENV RESTRICTIONS ==========
2421
+ // Drop denied credential env vars from the inherited environment. Emitted
2422
+ // before the proxy --setenv flags below: bwrap applies env operations in
2423
+ // argument order, so SRT's own proxy plumbing vars survive even if a
2424
+ // caller lists one of them as a denied credential.
2425
+ if (hasEnvRestrictions) {
2426
+ for (const name of unsetEnvVars ?? []) {
2427
+ bwrapArgs.push('--unsetenv', name);
2428
+ }
2429
+ // Masked credentials override the inherited real value with a
2430
+ // sentinel; bwrap --setenv replaces any inherited value of NAME.
2431
+ for (const [name, value] of Object.entries(setEnvVars ?? {})) {
2432
+ bwrapArgs.push('--setenv', name, value);
2433
+ }
2434
+ }
2435
+ // ========== GIT CONFIG (safe.directory) ==========
2436
+ // Under `--unshare-user` the repo owner's uid is unmapped so git
2437
+ // sees dubious ownership. `buildPosixGitSafeDirEnv` composes
2438
+ // against the child's actual starting env (process.env inherited,
2439
+ // unsetEnvVars dropped, setEnvVars overlaid) so an ambient
2440
+ // GIT_CONFIG_COUNT is continued, not clobbered.
2441
+ if (gitSafeDirectories && gitSafeDirectories.length > 0) {
2442
+ const gitCfg = buildPosixGitSafeDirEnv({
2443
+ safeDirs: gitSafeDirectories,
2444
+ unsetEnvVars,
2445
+ setEnvVars,
2446
+ });
2447
+ for (const [name, value] of Object.entries(gitCfg)) {
2448
+ bwrapArgs.push('--setenv', name, value);
2449
+ }
2450
+ }
2451
+ // ========== NETWORK RESTRICTIONS ==========
2452
+ if (needsNetworkRestriction) {
2453
+ // Always unshare network namespace to isolate network access
2454
+ // This removes all network interfaces, effectively blocking all network
2455
+ bwrapArgs.push('--unshare-net');
2456
+ // If proxy sockets are provided, bind them into the sandbox to allow
2457
+ // filtered network access through the proxy. If not provided, network
2458
+ // is completely blocked (empty allowedDomains = block all)
2459
+ if (httpSocketPath && socksSocketPath) {
2460
+ // Verify socket files still exist before trying to bind them
2461
+ if (!fs.existsSync(httpSocketPath)) {
2462
+ throw new Error(`Linux HTTP bridge socket does not exist: ${httpSocketPath}. ` +
2463
+ 'The bridge process may have died. Try reinitializing the sandbox.');
2464
+ }
2465
+ if (!fs.existsSync(socksSocketPath)) {
2466
+ throw new Error(`Linux SOCKS bridge socket does not exist: ${socksSocketPath}. ` +
2467
+ 'The bridge process may have died. Try reinitializing the sandbox.');
2468
+ }
2469
+ // Bind both sockets into the sandbox
2470
+ bwrapArgs.push('--bind', httpSocketPath, httpSocketPath);
2471
+ // When the mux serves both protocols, socksSocketPath is the same
2472
+ // file as httpSocketPath; bwrap rejects a duplicate --bind of the
2473
+ // same source→target.
2474
+ if (socksSocketPath !== httpSocketPath) {
2475
+ bwrapArgs.push('--bind', socksSocketPath, socksSocketPath);
2476
+ }
2477
+ // Add proxy environment variables
2478
+ // HTTP_PROXY points to the socat listener inside the sandbox (port 3128)
2479
+ // which forwards to the Unix socket that bridges to the host's proxy server
2480
+ const proxyEnv = generateProxyEnvVars(3128, // Internal HTTP listener port
2481
+ 1080, // Internal SOCKS listener port
2482
+ caCertPath, proxyAuthToken, writeConfig === undefined, attributionKey);
2483
+ bwrapArgs.push(...proxyEnv.flatMap((env) => {
2484
+ const firstEq = env.indexOf('=');
2485
+ const key = env.slice(0, firstEq);
2486
+ const value = env.slice(firstEq + 1);
2487
+ return ['--setenv', key, value];
2488
+ }));
2489
+ // JVMs ignore the proxy env vars above; the agent jar translates
2490
+ // them into system properties + an Authenticator at JVM start
2491
+ // (see java-proxy-agent.ts). Composed against the inherited value
2492
+ // so a caller's own JAVA_TOOL_OPTIONS survives.
2493
+ const javaToolOptions = buildJavaToolOptions({
2494
+ agentJarPath: javaAgentJarPath,
2495
+ unsetEnvVars,
2496
+ inherited: process.env.JAVA_TOOL_OPTIONS,
2497
+ });
2498
+ if (javaToolOptions !== undefined) {
2499
+ bwrapArgs.push('--setenv', 'JAVA_TOOL_OPTIONS', javaToolOptions);
2500
+ }
2501
+ // Add host proxy port environment variables for debugging/transparency
2502
+ // These show which host ports the Unix socket bridges connect to
2503
+ if (httpProxyPort !== undefined) {
2504
+ bwrapArgs.push('--setenv', 'CLAUDE_CODE_HOST_HTTP_PROXY_PORT', String(httpProxyPort));
2505
+ }
2506
+ if (socksProxyPort !== undefined) {
2507
+ bwrapArgs.push('--setenv', 'CLAUDE_CODE_HOST_SOCKS_PROXY_PORT', String(socksProxyPort));
2508
+ }
2509
+ }
2510
+ // If no sockets provided, network is completely blocked (--unshare-net without proxy)
2511
+ }
2512
+ // ========== FILESYSTEM RESTRICTIONS ==========
2513
+ const fsArgs = await generateFilesystemArgs(readConfig, writeConfig, maskedFileBinds, maskedFileStoreDir, ripgrepConfig, mandatoryDenySearchDepth, allowGitConfig, abortSignal);
2514
+ const mountsStart = bwrapArgs.length;
2515
+ bwrapArgs.push(...fsArgs);
2516
+ const mounts = { start: mountsStart, end: bwrapArgs.length };
2517
+ // Always bind /dev
2518
+ bwrapArgs.push('--dev', '/dev');
2519
+ // ========== PID NAMESPACE ISOLATION ==========
2520
+ // IMPORTANT: These must come AFTER filesystem binds for nested bwrap to work
2521
+ // By default, always unshare PID namespace and mount fresh /proc.
2522
+ // If we don't have --unshare-pid, it is possible to escape the sandbox.
2523
+ // If we don't have --proc, it is possible to read host /proc and leak information about code running
2524
+ // outside the sandbox. But, --proc is not available when running in unprivileged docker containers
2525
+ // so we support running without it if explicitly requested.
2526
+ bwrapArgs.push('--unshare-pid');
2527
+ // --unshare-user in both modes: bwrap only auto-creates a userns when
2528
+ // EUID != 0. A root parent in an unprivileged container (Docker's
2529
+ // default: EUID=0 without CAP_SYS_ADMIN) would otherwise try a direct
2530
+ // clone and EPERM.
2531
+ const euid = process.geteuid?.();
2532
+ const hasSetfcap = processHasBoundingCapability(CAP_SETFCAP);
2533
+ if (euid === 0 && !hasSetfcap && !setfcapMissingLogged) {
2534
+ setfcapMissingLogged = true;
2535
+ logForDebugging(`[Sandbox Linux] ${CAP_SETFCAP_MISSING_MESSAGE}`, {
2536
+ level: 'warn',
2537
+ });
2538
+ }
2539
+ bwrapArgs.push('--unshare-user', ...capabilityArgs({
2540
+ euid,
2541
+ hasSetfcap,
2542
+ usesSeccompHelper: applySeccompPrefix !== undefined,
2543
+ }));
2544
+ if (!enableWeakerNestedSandbox) {
2545
+ // Mount fresh /proc if PID namespace is isolated (secure mode).
2546
+ bwrapArgs.push('--proc', '/proc');
2547
+ }
2548
+ else {
2549
+ // --bind /proc /proc: apply-seccomp's nested-userns path writes
2550
+ // /proc/self/setgroups and uid_map. Without --proc above, the
2551
+ // --ro-bind / / leaves /proc read-only and those writes EROFS; a
2552
+ // fresh proc mount is what an unprivileged container refuses.
2553
+ bwrapArgs.push('--bind', '/proc', '/proc');
2554
+ }
2555
+ // apply-seccomp obtains CAP_SYS_ADMIN for its nested PID+mount unshare
2556
+ // by creating a nested user namespace. This requires the host to permit
2557
+ // capability-bearing unprivileged user namespaces (the same requirement
2558
+ // bwrap itself has when not installed setuid). See README for the
2559
+ // Ubuntu 24.04 sysctl if AppArmor restricts this.
2560
+ // ========== COMMAND ==========
2561
+ // Use the user's shell (zsh, bash, etc.) to ensure aliases/snapshots work
2562
+ // Resolve the full path to the shell binary since bwrap doesn't use $PATH
2563
+ const shellName = binShell || 'bash';
2564
+ const shell = whichSync(shellName);
2565
+ if (!shell) {
2566
+ throw new Error(`Shell '${shellName}' not found in PATH`);
2567
+ }
2568
+ bwrapArgs.push('--', shell, '-c');
2569
+ // With network restrictions, route the command through buildSandboxCommand
2570
+ // so socat starts before seccomp is applied. Otherwise invoke apply-seccomp
2571
+ // directly if we have a binary.
2572
+ if (needsNetworkRestriction && httpSocketPath && socksSocketPath) {
2573
+ const sandboxCommand = buildSandboxCommand(httpSocketPath, socksSocketPath, command, applySeccompPrefix, shell, socatPath);
2574
+ bwrapArgs.push(sandboxCommand);
2575
+ }
2576
+ else if (applySeccompPrefix) {
2577
+ const applySeccompCmd = applySeccompPrefix + quote([shell, '-c', command]);
2578
+ bwrapArgs.push(applySeccompCmd);
2579
+ }
2580
+ else {
2581
+ bwrapArgs.push(command);
2582
+ }
2583
+ const wrappedCommand = renderBwrapInvocation(bwrapPath ?? 'bwrap', bwrapArgs, mounts);
2584
+ const restrictions = [];
2585
+ if (needsNetworkRestriction)
2586
+ restrictions.push('network');
2587
+ if (hasReadRestrictions || hasWriteRestrictions)
2588
+ restrictions.push('filesystem');
2589
+ if (hasEnvRestrictions)
2590
+ restrictions.push('env');
2591
+ if (applySeccompPrefix)
2592
+ restrictions.push('seccomp(unix-block)');
2593
+ logForDebugging(`[Sandbox Linux] Wrapped command with bwrap (${restrictions.join(', ')} restrictions)`);
2594
+ return wrappedCommand;
2595
+ }
2596
+ catch (error) {
2597
+ // Undo the activeSandboxCount increment — the caller won't call
2598
+ // cleanupBwrapMountPoints() for a wrap that threw.
2599
+ if (activeSandboxCount > 0) {
2600
+ activeSandboxCount--;
2601
+ }
2602
+ throw error;
2603
+ }
2604
+ }
2605
+ //# sourceMappingURL=linux-sandbox-utils.js.map