apex-code 0.0.1-alpha.0 → 0.0.1-alpha.10

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 (739) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/README.md +74 -680
  3. package/dist/cli/args.d.ts +21 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +119 -15
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/auth-command.d.ts.map +1 -1
  8. package/dist/cli/auth-command.js +3 -3
  9. package/dist/cli/auth-command.js.map +1 -1
  10. package/dist/cli/concurrent-session.d.ts +3 -0
  11. package/dist/cli/concurrent-session.d.ts.map +1 -0
  12. package/dist/cli/concurrent-session.js +13 -0
  13. package/dist/cli/concurrent-session.js.map +1 -0
  14. package/dist/cli/cost-command.d.ts +19 -0
  15. package/dist/cli/cost-command.d.ts.map +1 -0
  16. package/dist/cli/cost-command.js +107 -0
  17. package/dist/cli/cost-command.js.map +1 -0
  18. package/dist/cli/file-processor.d.ts.map +1 -1
  19. package/dist/cli/file-processor.js +2 -1
  20. package/dist/cli/file-processor.js.map +1 -1
  21. package/dist/cli/startup-ui.d.ts +1 -1
  22. package/dist/cli/startup-ui.d.ts.map +1 -1
  23. package/dist/cli/startup-ui.js +1 -2
  24. package/dist/cli/startup-ui.js.map +1 -1
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +99 -16
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config.d.ts +5 -4
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +35 -20
  31. package/dist/config.js.map +1 -1
  32. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  33. package/dist/core/agent-session-runtime.js +2 -0
  34. package/dist/core/agent-session-runtime.js.map +1 -1
  35. package/dist/core/agent-session-services.d.ts +28 -0
  36. package/dist/core/agent-session-services.d.ts.map +1 -1
  37. package/dist/core/agent-session-services.js +140 -4
  38. package/dist/core/agent-session-services.js.map +1 -1
  39. package/dist/core/agent-session.d.ts +121 -9
  40. package/dist/core/agent-session.d.ts.map +1 -1
  41. package/dist/core/agent-session.js +364 -123
  42. package/dist/core/agent-session.js.map +1 -1
  43. package/dist/core/auth-storage.d.ts +4 -1
  44. package/dist/core/auth-storage.d.ts.map +1 -1
  45. package/dist/core/auth-storage.js +17 -14
  46. package/dist/core/auth-storage.js.map +1 -1
  47. package/dist/core/bash-executor.d.ts.map +1 -1
  48. package/dist/core/bash-executor.js +11 -10
  49. package/dist/core/bash-executor.js.map +1 -1
  50. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  51. package/dist/core/compaction/branch-summarization.js +3 -0
  52. package/dist/core/compaction/branch-summarization.js.map +1 -1
  53. package/dist/core/compaction/compaction.d.ts +4 -3
  54. package/dist/core/compaction/compaction.d.ts.map +1 -1
  55. package/dist/core/compaction/compaction.js +41 -32
  56. package/dist/core/compaction/compaction.js.map +1 -1
  57. package/dist/core/context/deferred-schemas.d.ts +92 -0
  58. package/dist/core/context/deferred-schemas.d.ts.map +1 -0
  59. package/dist/core/context/deferred-schemas.js +74 -0
  60. package/dist/core/context/deferred-schemas.js.map +1 -0
  61. package/dist/core/context/eviction.d.ts +47 -0
  62. package/dist/core/context/eviction.d.ts.map +1 -0
  63. package/dist/core/context/eviction.js +83 -0
  64. package/dist/core/context/eviction.js.map +1 -0
  65. package/dist/core/context/pipeline.d.ts +90 -0
  66. package/dist/core/context/pipeline.d.ts.map +1 -0
  67. package/dist/core/context/pipeline.js +109 -0
  68. package/dist/core/context/pipeline.js.map +1 -0
  69. package/dist/core/credential-failover.d.ts +30 -0
  70. package/dist/core/credential-failover.d.ts.map +1 -0
  71. package/dist/core/credential-failover.js +46 -0
  72. package/dist/core/credential-failover.js.map +1 -0
  73. package/dist/core/credential-pool.d.ts +60 -0
  74. package/dist/core/credential-pool.d.ts.map +1 -0
  75. package/dist/core/credential-pool.js +84 -0
  76. package/dist/core/credential-pool.js.map +1 -0
  77. package/dist/core/defaults.d.ts +1 -0
  78. package/dist/core/defaults.d.ts.map +1 -1
  79. package/dist/core/defaults.js +9 -0
  80. package/dist/core/defaults.js.map +1 -1
  81. package/dist/core/delegation/agents.d.ts +15 -0
  82. package/dist/core/delegation/agents.d.ts.map +1 -0
  83. package/dist/core/delegation/agents.js +55 -0
  84. package/dist/core/delegation/agents.js.map +1 -0
  85. package/dist/core/delegation/ceiling.d.ts +29 -0
  86. package/dist/core/delegation/ceiling.d.ts.map +1 -0
  87. package/dist/core/delegation/ceiling.js +34 -0
  88. package/dist/core/delegation/ceiling.js.map +1 -0
  89. package/dist/core/delegation/runtime.d.ts +94 -0
  90. package/dist/core/delegation/runtime.d.ts.map +1 -0
  91. package/dist/core/delegation/runtime.js +117 -0
  92. package/dist/core/delegation/runtime.js.map +1 -0
  93. package/dist/core/durable-state/daemon.d.ts +37 -0
  94. package/dist/core/durable-state/daemon.d.ts.map +1 -0
  95. package/dist/core/durable-state/daemon.js +62 -0
  96. package/dist/core/durable-state/daemon.js.map +1 -0
  97. package/dist/core/durable-state/provenance.d.ts +7 -0
  98. package/dist/core/durable-state/provenance.d.ts.map +1 -0
  99. package/dist/core/durable-state/provenance.js +24 -0
  100. package/dist/core/durable-state/provenance.js.map +1 -0
  101. package/dist/core/durable-state/sqlite.d.ts +84 -0
  102. package/dist/core/durable-state/sqlite.d.ts.map +1 -0
  103. package/dist/core/durable-state/sqlite.js +355 -0
  104. package/dist/core/durable-state/sqlite.js.map +1 -0
  105. package/dist/core/environment.d.ts +12 -0
  106. package/dist/core/environment.d.ts.map +1 -0
  107. package/dist/core/environment.js +69 -0
  108. package/dist/core/environment.js.map +1 -0
  109. package/dist/core/evidence-policy.d.ts +17 -0
  110. package/dist/core/evidence-policy.d.ts.map +1 -0
  111. package/dist/core/evidence-policy.js +25 -0
  112. package/dist/core/evidence-policy.js.map +1 -0
  113. package/dist/core/evidence.d.ts +16 -0
  114. package/dist/core/evidence.d.ts.map +1 -0
  115. package/dist/core/evidence.js +51 -0
  116. package/dist/core/evidence.js.map +1 -0
  117. package/dist/core/experimental.d.ts +4 -0
  118. package/dist/core/experimental.d.ts.map +1 -1
  119. package/dist/core/experimental.js +6 -1
  120. package/dist/core/experimental.js.map +1 -1
  121. package/dist/core/extensions/index.d.ts +2 -2
  122. package/dist/core/extensions/index.d.ts.map +1 -1
  123. package/dist/core/extensions/index.js +1 -1
  124. package/dist/core/extensions/index.js.map +1 -1
  125. package/dist/core/extensions/loader.d.ts.map +1 -1
  126. package/dist/core/extensions/loader.js +115 -48
  127. package/dist/core/extensions/loader.js.map +1 -1
  128. package/dist/core/extensions/types.d.ts +44 -9
  129. package/dist/core/extensions/types.d.ts.map +1 -1
  130. package/dist/core/extensions/types.js +3 -0
  131. package/dist/core/extensions/types.js.map +1 -1
  132. package/dist/core/footer-data-provider.d.ts +1 -1
  133. package/dist/core/footer-data-provider.d.ts.map +1 -1
  134. package/dist/core/footer-data-provider.js +1 -1
  135. package/dist/core/footer-data-provider.js.map +1 -1
  136. package/dist/core/http-dispatcher.d.ts +1 -2
  137. package/dist/core/http-dispatcher.d.ts.map +1 -1
  138. package/dist/core/http-dispatcher.js +2 -17
  139. package/dist/core/http-dispatcher.js.map +1 -1
  140. package/dist/core/http-idle-timeout.d.ts +12 -0
  141. package/dist/core/http-idle-timeout.d.ts.map +1 -0
  142. package/dist/core/http-idle-timeout.js +27 -0
  143. package/dist/core/http-idle-timeout.js.map +1 -0
  144. package/dist/core/index.d.ts +1 -0
  145. package/dist/core/index.d.ts.map +1 -1
  146. package/dist/core/index.js +1 -0
  147. package/dist/core/index.js.map +1 -1
  148. package/dist/core/keybindings.d.ts +38 -13
  149. package/dist/core/keybindings.d.ts.map +1 -1
  150. package/dist/core/keybindings.js +26 -5
  151. package/dist/core/keybindings.js.map +1 -1
  152. package/dist/core/lsp/client.d.ts +78 -0
  153. package/dist/core/lsp/client.d.ts.map +1 -0
  154. package/dist/core/lsp/client.js +494 -0
  155. package/dist/core/lsp/client.js.map +1 -0
  156. package/dist/core/lsp/diagnostics.d.ts +47 -0
  157. package/dist/core/lsp/diagnostics.d.ts.map +1 -0
  158. package/dist/core/lsp/diagnostics.js +224 -0
  159. package/dist/core/lsp/diagnostics.js.map +1 -0
  160. package/dist/core/lsp/pool.d.ts +48 -0
  161. package/dist/core/lsp/pool.d.ts.map +1 -0
  162. package/dist/core/lsp/pool.js +150 -0
  163. package/dist/core/lsp/pool.js.map +1 -0
  164. package/dist/core/lsp/registry.d.ts +49 -0
  165. package/dist/core/lsp/registry.d.ts.map +1 -0
  166. package/dist/core/lsp/registry.js +215 -0
  167. package/dist/core/lsp/registry.js.map +1 -0
  168. package/dist/core/lsp/status.d.ts +23 -0
  169. package/dist/core/lsp/status.d.ts.map +1 -0
  170. package/dist/core/lsp/status.js +35 -0
  171. package/dist/core/lsp/status.js.map +1 -0
  172. package/dist/core/lsp/sync.d.ts +13 -0
  173. package/dist/core/lsp/sync.d.ts.map +1 -0
  174. package/dist/core/lsp/sync.js +71 -0
  175. package/dist/core/lsp/sync.js.map +1 -0
  176. package/dist/core/model-config.d.ts +18 -0
  177. package/dist/core/model-config.d.ts.map +1 -1
  178. package/dist/core/model-config.js +25 -6
  179. package/dist/core/model-config.js.map +1 -1
  180. package/dist/core/model-resolver.d.ts +21 -0
  181. package/dist/core/model-resolver.d.ts.map +1 -1
  182. package/dist/core/model-resolver.js +73 -8
  183. package/dist/core/model-resolver.js.map +1 -1
  184. package/dist/core/model-runtime.d.ts +73 -0
  185. package/dist/core/model-runtime.d.ts.map +1 -1
  186. package/dist/core/model-runtime.js +225 -8
  187. package/dist/core/model-runtime.js.map +1 -1
  188. package/dist/core/models-store.d.ts.map +1 -1
  189. package/dist/core/models-store.js +2 -1
  190. package/dist/core/models-store.js.map +1 -1
  191. package/dist/core/observability/aggregate.d.ts +21 -0
  192. package/dist/core/observability/aggregate.d.ts.map +1 -0
  193. package/dist/core/observability/aggregate.js +36 -0
  194. package/dist/core/observability/aggregate.js.map +1 -0
  195. package/dist/core/observability/otlp.d.ts +66 -0
  196. package/dist/core/observability/otlp.d.ts.map +1 -0
  197. package/dist/core/observability/otlp.js +95 -0
  198. package/dist/core/observability/otlp.js.map +1 -0
  199. package/dist/core/package-manager.d.ts.map +1 -1
  200. package/dist/core/package-manager.js +40 -16
  201. package/dist/core/package-manager.js.map +1 -1
  202. package/dist/core/permissions/gate.d.ts +42 -0
  203. package/dist/core/permissions/gate.d.ts.map +1 -0
  204. package/dist/core/permissions/gate.js +72 -0
  205. package/dist/core/permissions/gate.js.map +1 -0
  206. package/dist/core/permissions/modes.d.ts +18 -0
  207. package/dist/core/permissions/modes.d.ts.map +1 -0
  208. package/dist/core/permissions/modes.js +57 -0
  209. package/dist/core/permissions/modes.js.map +1 -0
  210. package/dist/core/permissions/responder.d.ts +30 -0
  211. package/dist/core/permissions/responder.d.ts.map +1 -0
  212. package/dist/core/permissions/responder.js +32 -0
  213. package/dist/core/permissions/responder.js.map +1 -0
  214. package/dist/core/permissions/rules.d.ts +34 -0
  215. package/dist/core/permissions/rules.d.ts.map +1 -0
  216. package/dist/core/permissions/rules.js +74 -0
  217. package/dist/core/permissions/rules.js.map +1 -0
  218. package/dist/core/permissions/startup.d.ts +45 -0
  219. package/dist/core/permissions/startup.d.ts.map +1 -0
  220. package/dist/core/permissions/startup.js +63 -0
  221. package/dist/core/permissions/startup.js.map +1 -0
  222. package/dist/core/permissions/store.d.ts +114 -0
  223. package/dist/core/permissions/store.d.ts.map +1 -0
  224. package/dist/core/permissions/store.js +229 -0
  225. package/dist/core/permissions/store.js.map +1 -0
  226. package/dist/core/pi-manifest.d.ts.map +1 -1
  227. package/dist/core/pi-manifest.js +2 -1
  228. package/dist/core/pi-manifest.js.map +1 -1
  229. package/dist/core/project-trust.d.ts.map +1 -1
  230. package/dist/core/project-trust.js +1 -1
  231. package/dist/core/project-trust.js.map +1 -1
  232. package/dist/core/provider-attribution.d.ts.map +1 -1
  233. package/dist/core/provider-attribution.js +6 -7
  234. package/dist/core/provider-attribution.js.map +1 -1
  235. package/dist/core/remote-catalog-provider.d.ts +2 -2
  236. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  237. package/dist/core/remote-catalog-provider.js +22 -17
  238. package/dist/core/remote-catalog-provider.js.map +1 -1
  239. package/dist/core/resource-loader.d.ts.map +1 -1
  240. package/dist/core/resource-loader.js +3 -2
  241. package/dist/core/resource-loader.js.map +1 -1
  242. package/dist/core/sandbox/bwrap-arguments.d.ts +52 -0
  243. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  244. package/dist/core/sandbox/bwrap-arguments.js +86 -0
  245. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  246. package/dist/core/sandbox/child-entry.d.ts +2 -0
  247. package/dist/core/sandbox/child-entry.d.ts.map +1 -0
  248. package/dist/core/sandbox/child-entry.js +22 -0
  249. package/dist/core/sandbox/child-entry.js.map +1 -0
  250. package/dist/core/sandbox/cli-launch.d.ts +88 -0
  251. package/dist/core/sandbox/cli-launch.d.ts.map +1 -0
  252. package/dist/core/sandbox/cli-launch.js +272 -0
  253. package/dist/core/sandbox/cli-launch.js.map +1 -0
  254. package/dist/core/sandbox/cli-supervisor.d.ts +30 -0
  255. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -0
  256. package/dist/core/sandbox/cli-supervisor.js +123 -0
  257. package/dist/core/sandbox/cli-supervisor.js.map +1 -0
  258. package/dist/core/sandbox/default-hosts.d.ts +28 -0
  259. package/dist/core/sandbox/default-hosts.d.ts.map +1 -0
  260. package/dist/core/sandbox/default-hosts.js +69 -0
  261. package/dist/core/sandbox/default-hosts.js.map +1 -0
  262. package/dist/core/sandbox/full-access.d.ts +28 -0
  263. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  264. package/dist/core/sandbox/full-access.js +63 -0
  265. package/dist/core/sandbox/full-access.js.map +1 -0
  266. package/dist/core/sandbox/git-identity.d.ts +57 -0
  267. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  268. package/dist/core/sandbox/git-identity.js +78 -0
  269. package/dist/core/sandbox/git-identity.js.map +1 -0
  270. package/dist/core/sandbox/host-approval.d.ts +42 -0
  271. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  272. package/dist/core/sandbox/host-approval.js +99 -0
  273. package/dist/core/sandbox/host-approval.js.map +1 -0
  274. package/dist/core/sandbox/linux-backend.d.ts +46 -0
  275. package/dist/core/sandbox/linux-backend.d.ts.map +1 -0
  276. package/dist/core/sandbox/linux-backend.js +304 -0
  277. package/dist/core/sandbox/linux-backend.js.map +1 -0
  278. package/dist/core/sandbox/macos-backend.d.ts +28 -0
  279. package/dist/core/sandbox/macos-backend.d.ts.map +1 -0
  280. package/dist/core/sandbox/macos-backend.js +313 -0
  281. package/dist/core/sandbox/macos-backend.js.map +1 -0
  282. package/dist/core/sandbox/network-proxy.d.ts +38 -0
  283. package/dist/core/sandbox/network-proxy.d.ts.map +1 -0
  284. package/dist/core/sandbox/network-proxy.js +153 -0
  285. package/dist/core/sandbox/network-proxy.js.map +1 -0
  286. package/dist/core/sandbox/network-refusal.d.ts +24 -0
  287. package/dist/core/sandbox/network-refusal.d.ts.map +1 -0
  288. package/dist/core/sandbox/network-refusal.js +72 -0
  289. package/dist/core/sandbox/network-refusal.js.map +1 -0
  290. package/dist/core/sandbox/policy.d.ts +40 -0
  291. package/dist/core/sandbox/policy.d.ts.map +1 -0
  292. package/dist/core/sandbox/policy.js +55 -0
  293. package/dist/core/sandbox/policy.js.map +1 -0
  294. package/dist/core/sandbox/profiles.d.ts +25 -0
  295. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  296. package/dist/core/sandbox/profiles.js +23 -0
  297. package/dist/core/sandbox/profiles.js.map +1 -0
  298. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  299. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  300. package/dist/core/sandbox/rpc/command-client.js +86 -0
  301. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  302. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  303. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  304. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  305. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  306. package/dist/core/sandbox/rpc/credential-client.d.ts +20 -0
  307. package/dist/core/sandbox/rpc/credential-client.d.ts.map +1 -0
  308. package/dist/core/sandbox/rpc/credential-client.js +170 -0
  309. package/dist/core/sandbox/rpc/credential-client.js.map +1 -0
  310. package/dist/core/sandbox/rpc/credential-proxy.d.ts +44 -0
  311. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -0
  312. package/dist/core/sandbox/rpc/credential-proxy.js +287 -0
  313. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -0
  314. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  315. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  316. package/dist/core/sandbox/rpc/framing.js +114 -0
  317. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  318. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +40 -0
  319. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  320. package/dist/core/sandbox/rpc/git-credential-helper.js +152 -0
  321. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  322. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +48 -0
  323. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  324. package/dist/core/sandbox/rpc/git-credential-proxy.js +142 -0
  325. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  326. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  327. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  328. package/dist/core/sandbox/supervisor-temp.js +21 -0
  329. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  330. package/dist/core/sandbox/supervisor.d.ts +51 -0
  331. package/dist/core/sandbox/supervisor.d.ts.map +1 -0
  332. package/dist/core/sandbox/supervisor.js +18 -0
  333. package/dist/core/sandbox/supervisor.js.map +1 -0
  334. package/dist/core/sandbox/terminal-handoff.d.ts +45 -0
  335. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  336. package/dist/core/sandbox/terminal-handoff.js +176 -0
  337. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  338. package/dist/core/sandbox/terminal-size.d.ts +40 -0
  339. package/dist/core/sandbox/terminal-size.d.ts.map +1 -0
  340. package/dist/core/sandbox/terminal-size.js +105 -0
  341. package/dist/core/sandbox/terminal-size.js.map +1 -0
  342. package/dist/core/sandbox/violations.d.ts +23 -0
  343. package/dist/core/sandbox/violations.d.ts.map +1 -0
  344. package/dist/core/sandbox/violations.js +30 -0
  345. package/dist/core/sandbox/violations.js.map +1 -0
  346. package/dist/core/sdk.d.ts +32 -41
  347. package/dist/core/sdk.d.ts.map +1 -1
  348. package/dist/core/sdk.js +161 -6
  349. package/dist/core/sdk.js.map +1 -1
  350. package/dist/core/session-export.d.ts +4 -0
  351. package/dist/core/session-export.d.ts.map +1 -0
  352. package/dist/core/session-export.js +32 -0
  353. package/dist/core/session-export.js.map +1 -0
  354. package/dist/core/session-lease.d.ts +31 -0
  355. package/dist/core/session-lease.d.ts.map +1 -0
  356. package/dist/core/session-lease.js +138 -0
  357. package/dist/core/session-lease.js.map +1 -0
  358. package/dist/core/session-manager.d.ts +46 -2
  359. package/dist/core/session-manager.d.ts.map +1 -1
  360. package/dist/core/session-manager.js +62 -4
  361. package/dist/core/session-manager.js.map +1 -1
  362. package/dist/core/session-share.d.ts +31 -0
  363. package/dist/core/session-share.d.ts.map +1 -0
  364. package/dist/core/session-share.js +45 -0
  365. package/dist/core/session-share.js.map +1 -0
  366. package/dist/core/settings-diagnostics.d.ts +9 -0
  367. package/dist/core/settings-diagnostics.d.ts.map +1 -0
  368. package/dist/core/settings-diagnostics.js +21 -0
  369. package/dist/core/settings-diagnostics.js.map +1 -0
  370. package/dist/core/settings-manager.d.ts +92 -9
  371. package/dist/core/settings-manager.d.ts.map +1 -1
  372. package/dist/core/settings-manager.js +138 -36
  373. package/dist/core/settings-manager.js.map +1 -1
  374. package/dist/core/skills.d.ts +19 -7
  375. package/dist/core/skills.d.ts.map +1 -1
  376. package/dist/core/skills.js +128 -52
  377. package/dist/core/skills.js.map +1 -1
  378. package/dist/core/slash-commands.d.ts.map +1 -1
  379. package/dist/core/slash-commands.js +5 -2
  380. package/dist/core/slash-commands.js.map +1 -1
  381. package/dist/core/system-prompt.d.ts +2 -2
  382. package/dist/core/system-prompt.d.ts.map +1 -1
  383. package/dist/core/system-prompt.js +59 -68
  384. package/dist/core/system-prompt.js.map +1 -1
  385. package/dist/core/timings.d.ts +1 -1
  386. package/dist/core/timings.d.ts.map +1 -1
  387. package/dist/core/timings.js +3 -2
  388. package/dist/core/timings.js.map +1 -1
  389. package/dist/core/tools/ask-user.d.ts +25 -0
  390. package/dist/core/tools/ask-user.d.ts.map +1 -0
  391. package/dist/core/tools/ask-user.js +49 -0
  392. package/dist/core/tools/ask-user.js.map +1 -0
  393. package/dist/core/tools/bash-command-segments.d.ts +22 -0
  394. package/dist/core/tools/bash-command-segments.d.ts.map +1 -0
  395. package/dist/core/tools/bash-command-segments.js +128 -0
  396. package/dist/core/tools/bash-command-segments.js.map +1 -0
  397. package/dist/core/tools/bash.d.ts +40 -4
  398. package/dist/core/tools/bash.d.ts.map +1 -1
  399. package/dist/core/tools/bash.js +183 -32
  400. package/dist/core/tools/bash.js.map +1 -1
  401. package/dist/core/tools/contract.d.ts +183 -0
  402. package/dist/core/tools/contract.d.ts.map +1 -0
  403. package/dist/core/tools/contract.js +48 -0
  404. package/dist/core/tools/contract.js.map +1 -0
  405. package/dist/core/tools/delegate.d.ts +38 -0
  406. package/dist/core/tools/delegate.d.ts.map +1 -0
  407. package/dist/core/tools/delegate.js +90 -0
  408. package/dist/core/tools/delegate.js.map +1 -0
  409. package/dist/core/tools/diagnostics.d.ts +39 -0
  410. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  411. package/dist/core/tools/diagnostics.js +62 -0
  412. package/dist/core/tools/diagnostics.js.map +1 -0
  413. package/dist/core/tools/edit-diff.d.ts +0 -5
  414. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  415. package/dist/core/tools/edit-diff.js +2 -5
  416. package/dist/core/tools/edit-diff.js.map +1 -1
  417. package/dist/core/tools/edit.d.ts +12 -2
  418. package/dist/core/tools/edit.d.ts.map +1 -1
  419. package/dist/core/tools/edit.js +61 -10
  420. package/dist/core/tools/edit.js.map +1 -1
  421. package/dist/core/tools/find.d.ts +2 -2
  422. package/dist/core/tools/find.d.ts.map +1 -1
  423. package/dist/core/tools/find.js +13 -1
  424. package/dist/core/tools/find.js.map +1 -1
  425. package/dist/core/tools/grep.d.ts +4 -2
  426. package/dist/core/tools/grep.d.ts.map +1 -1
  427. package/dist/core/tools/grep.js +47 -10
  428. package/dist/core/tools/grep.js.map +1 -1
  429. package/dist/core/tools/index.d.ts +51 -6
  430. package/dist/core/tools/index.d.ts.map +1 -1
  431. package/dist/core/tools/index.js +145 -2
  432. package/dist/core/tools/index.js.map +1 -1
  433. package/dist/core/tools/ls.d.ts +4 -2
  434. package/dist/core/tools/ls.d.ts.map +1 -1
  435. package/dist/core/tools/ls.js +17 -1
  436. package/dist/core/tools/ls.js.map +1 -1
  437. package/dist/core/tools/lsp.d.ts +62 -0
  438. package/dist/core/tools/lsp.d.ts.map +1 -0
  439. package/dist/core/tools/lsp.js +216 -0
  440. package/dist/core/tools/lsp.js.map +1 -0
  441. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  442. package/dist/core/tools/output-accumulator.js +1 -1
  443. package/dist/core/tools/output-accumulator.js.map +1 -1
  444. package/dist/core/tools/path-permission.d.ts +18 -0
  445. package/dist/core/tools/path-permission.d.ts.map +1 -0
  446. package/dist/core/tools/path-permission.js +28 -0
  447. package/dist/core/tools/path-permission.js.map +1 -0
  448. package/dist/core/tools/plan-present.d.ts +27 -0
  449. package/dist/core/tools/plan-present.d.ts.map +1 -0
  450. package/dist/core/tools/plan-present.js +55 -0
  451. package/dist/core/tools/plan-present.js.map +1 -0
  452. package/dist/core/tools/powershell.d.ts +16 -0
  453. package/dist/core/tools/powershell.d.ts.map +1 -0
  454. package/dist/core/tools/powershell.js +39 -0
  455. package/dist/core/tools/powershell.js.map +1 -0
  456. package/dist/core/tools/read.d.ts +2 -2
  457. package/dist/core/tools/read.d.ts.map +1 -1
  458. package/dist/core/tools/read.js +14 -0
  459. package/dist/core/tools/read.js.map +1 -1
  460. package/dist/core/tools/skill-search.d.ts +32 -0
  461. package/dist/core/tools/skill-search.d.ts.map +1 -0
  462. package/dist/core/tools/skill-search.js +56 -0
  463. package/dist/core/tools/skill-search.js.map +1 -0
  464. package/dist/core/tools/test.d.ts +34 -0
  465. package/dist/core/tools/test.d.ts.map +1 -0
  466. package/dist/core/tools/test.js +75 -0
  467. package/dist/core/tools/test.js.map +1 -0
  468. package/dist/core/tools/todo-write.d.ts +31 -0
  469. package/dist/core/tools/todo-write.d.ts.map +1 -0
  470. package/dist/core/tools/todo-write.js +51 -0
  471. package/dist/core/tools/todo-write.js.map +1 -0
  472. package/dist/core/tools/tool-definition-wrapper.d.ts +13 -2
  473. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  474. package/dist/core/tools/tool-definition-wrapper.js +11 -1
  475. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  476. package/dist/core/tools/tool-schema.d.ts +22 -0
  477. package/dist/core/tools/tool-schema.d.ts.map +1 -0
  478. package/dist/core/tools/tool-schema.js +45 -0
  479. package/dist/core/tools/tool-schema.js.map +1 -0
  480. package/dist/core/tools/web-fetch.d.ts +26 -0
  481. package/dist/core/tools/web-fetch.d.ts.map +1 -0
  482. package/dist/core/tools/web-fetch.js +62 -0
  483. package/dist/core/tools/web-fetch.js.map +1 -0
  484. package/dist/core/tools/web-search-exa.d.ts +33 -0
  485. package/dist/core/tools/web-search-exa.d.ts.map +1 -0
  486. package/dist/core/tools/web-search-exa.js +128 -0
  487. package/dist/core/tools/web-search-exa.js.map +1 -0
  488. package/dist/core/tools/web-search.d.ts +26 -0
  489. package/dist/core/tools/web-search.d.ts.map +1 -0
  490. package/dist/core/tools/web-search.js +53 -0
  491. package/dist/core/tools/web-search.js.map +1 -0
  492. package/dist/core/tools/write.d.ts +16 -2
  493. package/dist/core/tools/write.d.ts.map +1 -1
  494. package/dist/core/tools/write.js +48 -2
  495. package/dist/core/tools/write.js.map +1 -1
  496. package/dist/core/trust-manager.d.ts.map +1 -1
  497. package/dist/core/trust-manager.js +2 -1
  498. package/dist/core/trust-manager.js.map +1 -1
  499. package/dist/core/usage-performance-store.d.ts +56 -0
  500. package/dist/core/usage-performance-store.d.ts.map +1 -0
  501. package/dist/core/usage-performance-store.js +85 -0
  502. package/dist/core/usage-performance-store.js.map +1 -0
  503. package/dist/core/web-search-provider.d.ts +64 -0
  504. package/dist/core/web-search-provider.d.ts.map +1 -0
  505. package/dist/core/web-search-provider.js +149 -0
  506. package/dist/core/web-search-provider.js.map +1 -0
  507. package/dist/extensions/llama/index.d.ts.map +1 -1
  508. package/dist/extensions/llama/index.js +2 -0
  509. package/dist/extensions/llama/index.js.map +1 -1
  510. package/dist/extensions/llama/provider.d.ts.map +1 -1
  511. package/dist/extensions/llama/provider.js +6 -2
  512. package/dist/extensions/llama/provider.js.map +1 -1
  513. package/dist/index.d.ts +7 -6
  514. package/dist/index.d.ts.map +1 -1
  515. package/dist/index.js +5 -4
  516. package/dist/index.js.map +1 -1
  517. package/dist/main.d.ts +5 -0
  518. package/dist/main.d.ts.map +1 -1
  519. package/dist/main.js +87 -22
  520. package/dist/main.js.map +1 -1
  521. package/dist/migrations.d.ts.map +1 -1
  522. package/dist/migrations.js +6 -5
  523. package/dist/migrations.js.map +1 -1
  524. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  525. package/dist/modes/interactive/components/config-selector.js +9 -6
  526. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  527. package/dist/modes/interactive/components/custom-editor.d.ts +72 -1
  528. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  529. package/dist/modes/interactive/components/custom-editor.js +338 -1
  530. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  531. package/dist/modes/interactive/components/dynamic-border.d.ts +7 -1
  532. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  533. package/dist/modes/interactive/components/dynamic-border.js +11 -2
  534. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  535. package/dist/modes/interactive/components/extension-selector.d.ts +4 -0
  536. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  537. package/dist/modes/interactive/components/extension-selector.js +24 -6
  538. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  539. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -4
  540. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  541. package/dist/modes/interactive/components/first-time-setup.js +17 -41
  542. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  543. package/dist/modes/interactive/components/first-use-hints.d.ts +8 -0
  544. package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -0
  545. package/dist/modes/interactive/components/first-use-hints.js +22 -0
  546. package/dist/modes/interactive/components/first-use-hints.js.map +1 -0
  547. package/dist/modes/interactive/components/footer.d.ts +23 -1
  548. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  549. package/dist/modes/interactive/components/footer.js +182 -21
  550. package/dist/modes/interactive/components/footer.js.map +1 -1
  551. package/dist/modes/interactive/components/index.d.ts +2 -0
  552. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  553. package/dist/modes/interactive/components/index.js +2 -0
  554. package/dist/modes/interactive/components/index.js.map +1 -1
  555. package/dist/modes/interactive/components/model-selector.d.ts +31 -9
  556. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  557. package/dist/modes/interactive/components/model-selector.js +236 -96
  558. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  559. package/dist/modes/interactive/components/settings-selector.d.ts +25 -5
  560. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  561. package/dist/modes/interactive/components/settings-selector.js +226 -58
  562. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  563. package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
  564. package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
  565. package/dist/modes/interactive/components/settings-submenu.js +165 -0
  566. package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
  567. package/dist/modes/interactive/components/splash-header.d.ts +66 -0
  568. package/dist/modes/interactive/components/splash-header.d.ts.map +1 -0
  569. package/dist/modes/interactive/components/splash-header.js +180 -0
  570. package/dist/modes/interactive/components/splash-header.js.map +1 -0
  571. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  572. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  573. package/dist/modes/interactive/components/thinking-selector.js +74 -17
  574. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  575. package/dist/modes/interactive/components/tool-execution.d.ts +16 -1
  576. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  577. package/dist/modes/interactive/components/tool-execution.js +113 -35
  578. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  579. package/dist/modes/interactive/components/tool-panel.d.ts +30 -0
  580. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -0
  581. package/dist/modes/interactive/components/tool-panel.js +130 -0
  582. package/dist/modes/interactive/components/tool-panel.js.map +1 -0
  583. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  584. package/dist/modes/interactive/external-editor.js +60 -10
  585. package/dist/modes/interactive/external-editor.js.map +1 -1
  586. package/dist/modes/interactive/interactive-mode.d.ts +61 -5
  587. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  588. package/dist/modes/interactive/interactive-mode.js +642 -203
  589. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  590. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  591. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  592. package/dist/modes/interactive/model-catalog-refresh.js +34 -0
  593. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  594. package/dist/modes/interactive/theme/apex.json +93 -0
  595. package/dist/modes/interactive/theme/dark.json +2 -0
  596. package/dist/modes/interactive/theme/light.json +3 -1
  597. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  598. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  599. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  600. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  601. package/dist/modes/interactive/theme/theme-schema.json +11 -3
  602. package/dist/modes/interactive/theme/theme.d.ts +14 -3
  603. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  604. package/dist/modes/interactive/theme/theme.js +65 -10
  605. package/dist/modes/interactive/theme/theme.js.map +1 -1
  606. package/dist/modes/json-event.d.ts +18 -12
  607. package/dist/modes/json-event.d.ts.map +1 -1
  608. package/dist/modes/json-event.js +22 -5
  609. package/dist/modes/json-event.js.map +1 -1
  610. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  611. package/dist/modes/rpc/rpc-mode.js +2 -1
  612. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  613. package/dist/package-manager-cli.d.ts +1 -0
  614. package/dist/package-manager-cli.d.ts.map +1 -1
  615. package/dist/package-manager-cli.js +211 -14
  616. package/dist/package-manager-cli.js.map +1 -1
  617. package/dist/rpc-entry.d.ts.map +1 -1
  618. package/dist/rpc-entry.js +2 -1
  619. package/dist/rpc-entry.js.map +1 -1
  620. package/dist/server/create-harness.d.ts +1 -1
  621. package/dist/server/create-harness.d.ts.map +1 -1
  622. package/dist/server/create-harness.js +8 -5
  623. package/dist/server/create-harness.js.map +1 -1
  624. package/dist/testing/replay/metrics.d.ts +24 -0
  625. package/dist/testing/replay/metrics.d.ts.map +1 -0
  626. package/dist/testing/replay/metrics.js +35 -0
  627. package/dist/testing/replay/metrics.js.map +1 -0
  628. package/dist/testing/replay/recorded-provider.d.ts +10 -0
  629. package/dist/testing/replay/recorded-provider.d.ts.map +1 -0
  630. package/dist/testing/replay/recorded-provider.js +108 -0
  631. package/dist/testing/replay/recorded-provider.js.map +1 -0
  632. package/dist/testing/replay/runner.d.ts +56 -0
  633. package/dist/testing/replay/runner.d.ts.map +1 -0
  634. package/dist/testing/replay/runner.js +406 -0
  635. package/dist/testing/replay/runner.js.map +1 -0
  636. package/dist/themes/apex-logo.d.ts +41 -0
  637. package/dist/themes/apex-logo.d.ts.map +1 -0
  638. package/dist/themes/apex-logo.js +35 -0
  639. package/dist/themes/apex-logo.js.map +1 -0
  640. package/dist/utils/apex-code-user-agent.d.ts +2 -0
  641. package/dist/utils/apex-code-user-agent.d.ts.map +1 -0
  642. package/dist/utils/apex-code-user-agent.js +5 -0
  643. package/dist/utils/apex-code-user-agent.js.map +1 -0
  644. package/dist/utils/changelog.d.ts +7 -2
  645. package/dist/utils/changelog.d.ts.map +1 -1
  646. package/dist/utils/changelog.js +82 -13
  647. package/dist/utils/changelog.js.map +1 -1
  648. package/dist/utils/clipboard-image.d.ts.map +1 -1
  649. package/dist/utils/clipboard-image.js +1 -1
  650. package/dist/utils/clipboard-image.js.map +1 -1
  651. package/dist/utils/frontmatter.d.ts.map +1 -1
  652. package/dist/utils/frontmatter.js +2 -1
  653. package/dist/utils/frontmatter.js.map +1 -1
  654. package/dist/utils/management-http.d.ts +5 -3
  655. package/dist/utils/management-http.d.ts.map +1 -1
  656. package/dist/utils/management-http.js +14 -10
  657. package/dist/utils/management-http.js.map +1 -1
  658. package/dist/utils/shell.d.ts +3 -0
  659. package/dist/utils/shell.d.ts.map +1 -1
  660. package/dist/utils/shell.js +17 -5
  661. package/dist/utils/shell.js.map +1 -1
  662. package/dist/utils/syntax-highlight.d.ts +1 -0
  663. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  664. package/dist/utils/syntax-highlight.js +60 -1
  665. package/dist/utils/syntax-highlight.js.map +1 -1
  666. package/dist/utils/text.d.ts +8 -0
  667. package/dist/utils/text.d.ts.map +1 -0
  668. package/dist/utils/text.js +9 -0
  669. package/dist/utils/text.js.map +1 -0
  670. package/dist/utils/tools-manager.d.ts +70 -2
  671. package/dist/utils/tools-manager.d.ts.map +1 -1
  672. package/dist/utils/tools-manager.js +280 -124
  673. package/dist/utils/tools-manager.js.map +1 -1
  674. package/dist/utils/version-check.d.ts +7 -5
  675. package/dist/utils/version-check.d.ts.map +1 -1
  676. package/dist/utils/version-check.js +25 -14
  677. package/dist/utils/version-check.js.map +1 -1
  678. package/dist/utils/windows-self-update.d.ts.map +1 -1
  679. package/dist/utils/windows-self-update.js +1 -1
  680. package/dist/utils/windows-self-update.js.map +1 -1
  681. package/docs/compaction.md +39 -24
  682. package/docs/containerization.md +24 -21
  683. package/docs/custom-provider.md +15 -12
  684. package/docs/development.md +11 -11
  685. package/docs/environment-variables.md +37 -77
  686. package/docs/extensions.md +68 -54
  687. package/docs/index.md +12 -24
  688. package/docs/json.md +18 -11
  689. package/docs/keybindings.md +18 -12
  690. package/docs/llama-cpp.md +9 -7
  691. package/docs/models.md +17 -11
  692. package/docs/packages.md +47 -43
  693. package/docs/prompt-templates.md +3 -3
  694. package/docs/providers.md +10 -10
  695. package/docs/quickstart.md +49 -32
  696. package/docs/rpc.md +35 -18
  697. package/docs/sdk.md +20 -6
  698. package/docs/security.md +18 -18
  699. package/docs/session-format.md +9 -9
  700. package/docs/sessions.md +13 -13
  701. package/docs/settings.md +83 -16
  702. package/docs/shell-aliases.md +1 -1
  703. package/docs/skills.md +66 -15
  704. package/docs/terminal-setup.md +44 -19
  705. package/docs/termux.md +4 -4
  706. package/docs/themes.md +32 -11
  707. package/docs/tmux.md +4 -4
  708. package/docs/tui.md +5 -5
  709. package/docs/usage.md +61 -53
  710. package/docs/windows.md +24 -2
  711. package/examples/extensions/border-status-editor.ts +1 -1
  712. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  713. package/examples/extensions/custom-provider-anthropic/package.json +2 -2
  714. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  715. package/examples/extensions/git-checkpoint.ts +2 -2
  716. package/examples/extensions/gondolin/package-lock.json +2 -2
  717. package/examples/extensions/gondolin/package.json +1 -1
  718. package/examples/extensions/notify.ts +3 -1
  719. package/examples/extensions/sandbox/package-lock.json +2 -2
  720. package/examples/extensions/sandbox/package.json +1 -1
  721. package/examples/extensions/subagent/README.md +3 -1
  722. package/examples/extensions/subagent/agents.ts +40 -9
  723. package/examples/extensions/subagent/index.ts +27 -4
  724. package/examples/extensions/titlebar-spinner.ts +1 -1
  725. package/examples/extensions/with-deps/package-lock.json +2 -2
  726. package/examples/extensions/with-deps/package.json +1 -1
  727. package/examples/rpc-extension-ui.ts +1 -1
  728. package/examples/sdk/06-extensions.ts +1 -1
  729. package/examples/sdk/README.md +1 -1
  730. package/npm-shrinkwrap.json +280 -445
  731. package/package.json +9 -12
  732. package/dist/core/telemetry.d.ts +0 -3
  733. package/dist/core/telemetry.d.ts.map +0 -1
  734. package/dist/core/telemetry.js +0 -9
  735. package/dist/core/telemetry.js.map +0 -1
  736. package/dist/utils/pi-user-agent.d.ts +0 -2
  737. package/dist/utils/pi-user-agent.d.ts.map +0 -1
  738. package/dist/utils/pi-user-agent.js +0 -5
  739. package/dist/utils/pi-user-agent.js.map +0 -1
@@ -0,0 +1,46 @@
1
+ import { spawn } from "node:child_process";
2
+ import { type CommandEscalationRequest } from "./rpc/command-proxy.ts";
3
+ import { fillHostGitCredential } from "./rpc/git-credential-helper.ts";
4
+ import type { SandboxBackend } from "./supervisor.ts";
5
+ import type { SandboxViolationStore } from "./violations.ts";
6
+ export interface LinuxSandboxBackendOptions {
7
+ /** Injectable only for preflight tests; production uses the running platform. */
8
+ platform?: NodeJS.Platform;
9
+ commandExists?: (command: string) => boolean;
10
+ violationStore?: SandboxViolationStore;
11
+ /** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */
12
+ spawnChild?: typeof spawn;
13
+ /**
14
+ * Injectable only for tests, which have no terminal and would otherwise be unable to
15
+ * exercise a released credential at all. Production builds the releaser from the
16
+ * terminal handoff, and its absence there is what makes a headless session refuse.
17
+ */
18
+ requestGitCredentialRelease?: (host: string) => Promise<boolean>;
19
+ /** Injectable only for tests; production resolves against the real host store. */
20
+ fillGitCredential?: typeof fillHostGitCredential;
21
+ /** Injectable only for tests, which have no terminal to approve an escalation at. */
22
+ requestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;
23
+ }
24
+ /**
25
+ * Where the supervisor's proxy socket lives on each side of the boundary.
26
+ *
27
+ * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:
28
+ * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to
29
+ * start at all with `listen EINVAL`. The host side therefore gets a short, unique path
30
+ * under the system temp directory, and it is bind-mounted into the child.
31
+ *
32
+ * The child-side path has to sit under `/home`. That is the one writable mount at the
33
+ * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap
34
+ * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.
35
+ */
36
+ export declare function resolveProxySocketPaths(temporaryDirectory?: string): {
37
+ hostSocketPath: string;
38
+ childSocketPath: string;
39
+ };
40
+ /**
41
+ * Linux's deliberately small platform adapter. The sandbox starts from a read-only
42
+ * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`
43
+ * removes all direct network interfaces for the child and every descendant.
44
+ */
45
+ export declare function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend;
46
+ //# sourceMappingURL=linux-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linux-backend.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAQtD,OAAO,EAEN,KAAK,wBAAwB,EAI7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,qBAAqB,EAIrB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK7D,MAAM,WAAW,0BAA0B;IAC1C,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,2FAA2F;IAC3F,UAAU,CAAC,EAAE,OAAO,KAAK,CAAC;IAC1B;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,OAAO,qBAAqB,CAAC;IACjD,qFAAqF;IACrF,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnF;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,kBAAkB,GAAE,MAAiB,GAAG;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACxB,CAKA;AAmBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CA+M9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport { createTerminalHandoff, TERMINAL_HANDOFF_PATH_VARIABLE, type TerminalHandoff } from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst stateDirectory = join(launch.policy.workspace, \".apex-code\", \"sandbox-state\");\n\t\t\tmkdirSync(stateDirectory, { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(stateDirectory);\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(stateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(stateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(stateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])],\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: stateDirectory,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: launch.readOnlyPaths ?? [],\n\t\t\treadOnlyFiles: [],\n\t\t\treadOnlyBinaries: [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\"] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\tchild.stdout?.setEncoding(\"utf8\");\n\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\tstdout += chunk;\n\t});\n\tchild.stderr?.setEncoding(\"utf8\");\n\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\tstderr += chunk;\n\t});\n\tconst code = await new Promise<number>((resolveCode) => {\n\t\tchild.once(\"error\", () => resolveCode(1));\n\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t});\n\treturn { code, stdout, stderr };\n}\n"]}
@@ -0,0 +1,304 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { buildBwrapArguments } from "./bwrap-arguments.js";
7
+ import { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from "./host-approval.js";
8
+ import { createSandboxNetworkProxy } from "./network-proxy.js";
9
+ import { COMMAND_ESCALATION_SOCKET_VARIABLE, createCommandEscalationProxy, resolveCommandEscalationChannelPaths, } from "./rpc/command-proxy.js";
10
+ import { fillHostGitCredential, GIT_CREDENTIAL_SOCKET_VARIABLE, resolveGitCredentialChannelPaths, writeGitCredentialHelper, } from "./rpc/git-credential-helper.js";
11
+ import { createGitCredentialProxy } from "./rpc/git-credential-proxy.js";
12
+ import { createTerminalHandoff, TERMINAL_HANDOFF_PATH_VARIABLE } from "./terminal-handoff.js";
13
+ import { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from "./terminal-size.js";
14
+ /** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */
15
+ const SUN_PATH_LIMIT = 108;
16
+ function commandExists(command) {
17
+ const result = spawnSync(command, ["--version"], { stdio: "ignore", timeout: 1_000 });
18
+ return result.status === 0;
19
+ }
20
+ function waitForExit(child) {
21
+ return new Promise((resolve, reject) => {
22
+ child.once("error", reject);
23
+ child.once("exit", (code) => resolve(code ?? 1));
24
+ });
25
+ }
26
+ /**
27
+ * Where the supervisor's proxy socket lives on each side of the boundary.
28
+ *
29
+ * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:
30
+ * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to
31
+ * start at all with `listen EINVAL`. The host side therefore gets a short, unique path
32
+ * under the system temp directory, and it is bind-mounted into the child.
33
+ *
34
+ * The child-side path has to sit under `/home`. That is the one writable mount at the
35
+ * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap
36
+ * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.
37
+ */
38
+ export function resolveProxySocketPaths(temporaryDirectory = tmpdir()) {
39
+ const name = `apex-${process.pid}-${randomBytes(4).toString("hex")}.sock`;
40
+ // An unusually long TMPDIR would reintroduce the very limit this avoids.
41
+ const base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? "/tmp" : temporaryDirectory;
42
+ return { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };
43
+ }
44
+ /**
45
+ * Classify an unsuccessful child from what its stderr actually evidences, or return
46
+ * undefined when nothing suggests the boundary refused anything.
47
+ *
48
+ * A non-zero exit is not a violation. An invalid API key, a failing test command, or a
49
+ * script exiting 1 are the child's own business, and recording them as sandbox
50
+ * violations blames the boundary for failures it had no part in — which is worse than
51
+ * silence, because it sends whoever reads the output looking for a refusal that never
52
+ * happened. Only Linux's own refusal wording earns a violation here; the proxy records
53
+ * network refusals itself, before this fallback is consulted.
54
+ */
55
+ function classifySandboxFailure(stderr) {
56
+ if (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr))
57
+ return "filesystem";
58
+ if (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr))
59
+ return "network";
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Linux's deliberately small platform adapter. The sandbox starts from a read-only
64
+ * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`
65
+ * removes all direct network interfaces for the child and every descendant.
66
+ */
67
+ export function createLinuxSandboxBackend(options) {
68
+ const platform = options?.platform ?? process.platform;
69
+ if (platform !== "linux") {
70
+ return {
71
+ status: { kind: "unavailable", reason: "OS sandbox is supported on Linux only." },
72
+ async launch() {
73
+ throw new Error("Linux sandbox backend is unavailable.");
74
+ },
75
+ async close() { },
76
+ };
77
+ }
78
+ const hasCommand = options?.commandExists ?? commandExists;
79
+ const violationStore = options?.violationStore;
80
+ let proxy;
81
+ let handoff;
82
+ let gitCredentialProxy;
83
+ let gitCredentialDirectory;
84
+ let escalationProxy;
85
+ let escalationDirectory;
86
+ if (!hasCommand("bwrap")) {
87
+ return {
88
+ status: { kind: "unavailable", reason: "Bubblewrap (bwrap) is required for OS sandboxing." },
89
+ async launch() {
90
+ throw new Error("Linux sandbox backend is unavailable.");
91
+ },
92
+ async close() { },
93
+ };
94
+ }
95
+ return {
96
+ status: { kind: "enforced" },
97
+ async launch(launch) {
98
+ const stateDirectory = join(launch.policy.workspace, ".apex-code", "sandbox-state");
99
+ mkdirSync(stateDirectory, { recursive: true });
100
+ const violationCountBeforeLaunch = violationStore?.totalCount ?? 0;
101
+ const { hostSocketPath, childSocketPath } = resolveProxySocketPaths();
102
+ handoff = createTerminalHandoff(stateDirectory);
103
+ const gitCredentialPaths = resolveGitCredentialChannelPaths();
104
+ gitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;
105
+ proxy = await createSandboxNetworkProxy({
106
+ socketPath: hostSocketPath,
107
+ allowedHosts: launch.policy.allowedHosts,
108
+ violationStore,
109
+ // Undefined without a terminal, which leaves the proxy's own
110
+ // deny-without-asking path in place for headless, print, JSON, and RPC.
111
+ requestApproval: createHostApprover({ handoff }),
112
+ });
113
+ // After the network proxy, because reachability is its answer to give: the
114
+ // credential channel must never hand out a token for a host this session
115
+ // cannot even open a connection to.
116
+ gitCredentialProxy = await createGitCredentialProxy({
117
+ socketPath: gitCredentialPaths.hostSocketPath,
118
+ isHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,
119
+ requestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),
120
+ // The supervisor's own environment, deliberately: this runs outside the
121
+ // boundary and must resolve against the real host home, which is exactly
122
+ // what the child cannot see.
123
+ fillCredential: (request) => (options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),
124
+ violationStore,
125
+ });
126
+ writeGitCredentialHelper(stateDirectory);
127
+ const escalationPaths = resolveCommandEscalationChannelPaths();
128
+ escalationDirectory = escalationPaths.hostSocketDirectory;
129
+ // .cjs forces CommonJS regardless of the target workspace's package.json "type"
130
+ // field -- a plain .js here would be parsed as ESM under "type": "module" and
131
+ // crash on `require`, since Node resolves module type from the nearest package.json.
132
+ const relayScriptPath = join(stateDirectory, "relay.cjs");
133
+ writeFileSync(relayScriptPath, `
134
+ const net = require("node:net");
135
+ const child_process = require("node:child_process");
136
+
137
+ const socketPath = process.env.APEX_UDS_PATH;
138
+ const server = net.createServer((c) => {
139
+ const client = net.connect(socketPath);
140
+ c.pipe(client).pipe(c);
141
+ c.on("error", () => {});
142
+ client.on("error", () => {});
143
+ });
144
+
145
+ server.listen(0, "127.0.0.1", () => {
146
+ const port = server.address().port;
147
+ const env = Object.assign({}, process.env, {
148
+ HTTP_PROXY: \`http://127.0.0.1:\${port}\`,
149
+ HTTPS_PROXY: \`http://127.0.0.1:\${port}\`
150
+ });
151
+ const args = process.argv.slice(2);
152
+ const child = child_process.spawn(args[0], args.slice(1), {
153
+ stdio: "inherit",
154
+ env: env,
155
+ });
156
+ child.on("exit", (code, signal) => {
157
+ process.exit(code ?? (signal ? 128 : 1));
158
+ });
159
+ });
160
+ server.on("error", (err) => {
161
+ console.error("Relay error:", err);
162
+ process.exit(1);
163
+ });
164
+ `.trim());
165
+ const readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, "r"));
166
+ // The descriptors above are opened before the child spawns and must be closed
167
+ // on every exit path -- including a spawn/wait rejection -- or a crashed launch
168
+ // leaks an open handle onto a host-owned credential file for the process's life.
169
+ // The workspace is bind-mounted read-write into the sandbox, so a file
170
+ // here is visible at the same absolute path on both sides of it.
171
+ const terminalSizePath = join(stateDirectory, "terminal-size");
172
+ const stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);
173
+ // The second child derives its argv from the same builder as the first, with one
174
+ // extra writable root. Nothing about the original child's namespace changes.
175
+ escalationProxy = await createCommandEscalationProxy({
176
+ socketPath: escalationPaths.hostSocketPath,
177
+ requestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),
178
+ violationStore,
179
+ runEscalated: (request) => runEscalatedCommand({
180
+ request,
181
+ launch,
182
+ stateDirectory,
183
+ spawnBwrap: options?.spawnChild ?? spawn,
184
+ }),
185
+ });
186
+ try {
187
+ const spawnBwrap = options?.spawnChild ?? spawn;
188
+ const child = spawnBwrap("bwrap", buildBwrapArguments({
189
+ workspace: launch.policy.workspace,
190
+ additionalWritableRoots: launch.policy.additionalWritableRoots,
191
+ readOnlyPaths: [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])],
192
+ readOnlyFiles: launch.readOnlyFiles ?? [],
193
+ readOnlyBinaries: launch.readOnlyBinaries ?? [],
194
+ sockets: [
195
+ { hostPath: hostSocketPath, childPath: childSocketPath },
196
+ // Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.
197
+ ...(launch.credentialChannel
198
+ ? [
199
+ {
200
+ hostPath: launch.credentialChannel.hostSocketPath,
201
+ childPath: launch.credentialChannel.childSocketPath,
202
+ },
203
+ ]
204
+ : []),
205
+ {
206
+ hostPath: gitCredentialPaths.hostSocketPath,
207
+ childPath: gitCredentialPaths.childSocketPath,
208
+ },
209
+ ],
210
+ environment: {
211
+ HOME: launch.environment?.HOME ?? stateDirectory,
212
+ TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
213
+ APEX_UDS_PATH: childSocketPath,
214
+ [TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,
215
+ [TERMINAL_HANDOFF_PATH_VARIABLE]: stateDirectory,
216
+ [GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,
217
+ [COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,
218
+ },
219
+ command: process.execPath,
220
+ args: [relayScriptPath, launch.command, ...launch.args],
221
+ }), { env: launch.environment, stdio: ["inherit", "inherit", "pipe", ...readOnlyFileDescriptors] });
222
+ let stderr = "";
223
+ child.stderr?.setEncoding("utf8");
224
+ child.stderr?.on("data", (chunk) => {
225
+ stderr += chunk;
226
+ process.stderr.write(chunk);
227
+ });
228
+ const exitCode = await waitForExit(child);
229
+ await proxy?.close();
230
+ const proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;
231
+ const kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;
232
+ if (kind) {
233
+ violationStore?.add({
234
+ kind,
235
+ command: [launch.command, ...launch.args].join(" "),
236
+ detail: stderr.trim(),
237
+ timestamp: new Date(),
238
+ });
239
+ }
240
+ return exitCode;
241
+ }
242
+ finally {
243
+ stopPublishingTerminalSize();
244
+ for (const descriptor of readOnlyFileDescriptors)
245
+ closeSync(descriptor);
246
+ // The socket now lives outside the workspace, so nothing else will ever clean
247
+ // it up; leaving it would litter the temp directory once per launch.
248
+ rmSync(hostSocketPath, { force: true });
249
+ }
250
+ },
251
+ async close() {
252
+ handoff?.stop();
253
+ await escalationProxy?.close();
254
+ if (escalationDirectory)
255
+ rmSync(escalationDirectory, { force: true, recursive: true });
256
+ await gitCredentialProxy?.close();
257
+ if (gitCredentialDirectory)
258
+ rmSync(gitCredentialDirectory, { force: true, recursive: true });
259
+ await proxy?.close();
260
+ },
261
+ };
262
+ }
263
+ /**
264
+ * Start one approved command in its own child, with one extra writable root.
265
+ *
266
+ * Deliberately minimal compared with the session child: no network relay, no credential
267
+ * channels, no terminal handoff. An escalated command is a single shell invocation the
268
+ * human just read and approved, not a session, and every channel omitted here is one it
269
+ * cannot reach. Its stdio is captured rather than inherited so the session that asked
270
+ * receives the output instead of the escalated process writing over the TUI.
271
+ */
272
+ async function runEscalatedCommand(options) {
273
+ const { request, launch, stateDirectory } = options;
274
+ const child = options.spawnBwrap("bwrap", buildBwrapArguments({
275
+ workspace: launch.policy.workspace,
276
+ additionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],
277
+ readOnlyPaths: launch.readOnlyPaths ?? [],
278
+ readOnlyFiles: [],
279
+ readOnlyBinaries: [],
280
+ sockets: [],
281
+ environment: {
282
+ HOME: launch.environment?.HOME ?? stateDirectory,
283
+ TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
284
+ },
285
+ command: "/bin/sh",
286
+ args: ["-c", request.command],
287
+ }), { stdio: ["ignore", "pipe", "pipe"] });
288
+ let stdout = "";
289
+ let stderr = "";
290
+ child.stdout?.setEncoding("utf8");
291
+ child.stdout?.on("data", (chunk) => {
292
+ stdout += chunk;
293
+ });
294
+ child.stderr?.setEncoding("utf8");
295
+ child.stderr?.on("data", (chunk) => {
296
+ stderr += chunk;
297
+ });
298
+ const code = await new Promise((resolveCode) => {
299
+ child.once("error", () => resolveCode(1));
300
+ child.once("exit", (exitCode) => resolveCode(exitCode ?? 1));
301
+ });
302
+ return { code, stdout, stderr };
303
+ }
304
+ //# sourceMappingURL=linux-backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linux-backend.js","sourceRoot":"","sources":["../../../src/core/sandbox/linux-backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { closeSync, mkdirSync, openSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { buildBwrapArguments } from \"./bwrap-arguments.ts\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\ttype CommandEscalationResult,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport { createTerminalHandoff, TERMINAL_HANDOFF_PATH_VARIABLE, type TerminalHandoff } from \"./terminal-handoff.ts\";\nimport { publishTerminalSize, TERMINAL_SIZE_PATH_VARIABLE } from \"./terminal-size.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\n/** AF_UNIX `sun_path` is 108 bytes on Linux, including the terminating NUL. */\nconst SUN_PATH_LIMIT = 108;\n\nexport interface LinuxSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/** Injectable only to prove crash cleanup in tests; production spawns `bwrap` directly. */\n\tspawnChild?: typeof spawn;\n\t/**\n\t * Injectable only for tests, which have no terminal and would otherwise be unable to\n\t * exercise a released credential at all. Production builds the releaser from the\n\t * terminal handoff, and its absence there is what makes a headless session refuse.\n\t */\n\trequestGitCredentialRelease?: (host: string) => Promise<boolean>;\n\t/** Injectable only for tests; production resolves against the real host store. */\n\tfillGitCredential?: typeof fillHostGitCredential;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n}\n\nfunction commandExists(command: string): boolean {\n\tconst result = spawnSync(command, [\"--version\"], { stdio: \"ignore\", timeout: 1_000 });\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Where the supervisor's proxy socket lives on each side of the boundary.\n *\n * AF_UNIX caps `sun_path` at 108 bytes, so the socket cannot live under the workspace:\n * a deep-but-ordinary project directory exhausts that budget and the sandbox fails to\n * start at all with `listen EINVAL`. The host side therefore gets a short, unique path\n * under the system temp directory, and it is bind-mounted into the child.\n *\n * The child-side path has to sit under `/home`. That is the one writable mount at the\n * point the mountpoint is created — the sandbox root is a read-only bind, so bwrap\n * cannot create a mountpoint at `/run`, `/tmp`, or anywhere else on it.\n */\nexport function resolveProxySocketPaths(temporaryDirectory: string = tmpdir()): {\n\thostSocketPath: string;\n\tchildSocketPath: string;\n} {\n\tconst name = `apex-${process.pid}-${randomBytes(4).toString(\"hex\")}.sock`;\n\t// An unusually long TMPDIR would reintroduce the very limit this avoids.\n\tconst base = join(temporaryDirectory, name).length + 1 > SUN_PATH_LIMIT ? \"/tmp\" : temporaryDirectory;\n\treturn { hostSocketPath: join(base, name), childSocketPath: `/home/${name}` };\n}\n\n/**\n * Classify an unsuccessful child from what its stderr actually evidences, or return\n * undefined when nothing suggests the boundary refused anything.\n *\n * A non-zero exit is not a violation. An invalid API key, a failing test command, or a\n * script exiting 1 are the child's own business, and recording them as sandbox\n * violations blames the boundary for failures it had no part in — which is worse than\n * silence, because it sends whoever reads the output looking for a refusal that never\n * happened. Only Linux's own refusal wording earns a violation here; the proxy records\n * network refusals itself, before this fallback is consulted.\n */\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | undefined {\n\tif (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr)) return \"network\";\n\treturn undefined;\n}\n\n/**\n * Linux's deliberately small platform adapter. The sandbox starts from a read-only\n * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`\n * removes all direct network interfaces for the child and every descendant.\n */\nexport function createLinuxSandboxBackend(options?: LinuxSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"linux\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on Linux only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? commandExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\n\tif (!hasCommand(\"bwrap\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"Bubblewrap (bwrap) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"Linux sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\tconst stateDirectory = join(launch.policy.workspace, \".apex-code\", \"sandbox-state\");\n\t\t\tmkdirSync(stateDirectory, { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\tconst { hostSocketPath, childSocketPath } = resolveProxySocketPaths();\n\t\t\thandoff = createTerminalHandoff(stateDirectory);\n\t\t\tconst gitCredentialPaths = resolveGitCredentialChannelPaths();\n\t\t\tgitCredentialDirectory = gitCredentialPaths.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath: hostSocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give: the\n\t\t\t// credential channel must never hand out a token for a host this session\n\t\t\t// cannot even open a connection to.\n\t\t\tgitCredentialProxy = await createGitCredentialProxy({\n\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\trequestRelease: options?.requestGitCredentialRelease ?? createCredentialReleaser({ handoff }),\n\t\t\t\t// The supervisor's own environment, deliberately: this runs outside the\n\t\t\t\t// boundary and must resolve against the real host home, which is exactly\n\t\t\t\t// what the child cannot see.\n\t\t\t\tfillCredential: (request) =>\n\t\t\t\t\t(options?.fillGitCredential ?? fillHostGitCredential)(request, { environment: process.env }),\n\t\t\t\tviolationStore,\n\t\t\t});\n\t\t\twriteGitCredentialHelper(stateDirectory);\n\n\t\t\tconst escalationPaths = resolveCommandEscalationChannelPaths();\n\t\t\tescalationDirectory = escalationPaths.hostSocketDirectory;\n\n\t\t\t// .cjs forces CommonJS regardless of the target workspace's package.json \"type\"\n\t\t\t// field -- a plain .js here would be parsed as ESM under \"type\": \"module\" and\n\t\t\t// crash on `require`, since Node resolves module type from the nearest package.json.\n\t\t\tconst relayScriptPath = join(stateDirectory, \"relay.cjs\");\n\t\t\twriteFileSync(\n\t\t\t\trelayScriptPath,\n\t\t\t\t`\nconst net = require(\"node:net\");\nconst child_process = require(\"node:child_process\");\n\nconst socketPath = process.env.APEX_UDS_PATH;\nconst server = net.createServer((c) => {\n\tconst client = net.connect(socketPath);\n\tc.pipe(client).pipe(c);\n\tc.on(\"error\", () => {});\n\tclient.on(\"error\", () => {});\n});\n\nserver.listen(0, \"127.0.0.1\", () => {\n\tconst port = server.address().port;\n\tconst env = Object.assign({}, process.env, {\n\t\tHTTP_PROXY: \\`http://127.0.0.1:\\${port}\\`,\n\t\tHTTPS_PROXY: \\`http://127.0.0.1:\\${port}\\`\n\t});\n\tconst args = process.argv.slice(2);\n\tconst child = child_process.spawn(args[0], args.slice(1), {\n\t\tstdio: \"inherit\",\n\t\tenv: env,\n\t});\n\tchild.on(\"exit\", (code, signal) => {\n\t\tprocess.exit(code ?? (signal ? 128 : 1));\n\t});\n});\nserver.on(\"error\", (err) => {\n\tconsole.error(\"Relay error:\", err);\n\tprocess.exit(1);\n});\n`.trim(),\n\t\t\t);\n\n\t\t\tconst readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, \"r\"));\n\t\t\t// The descriptors above are opened before the child spawns and must be closed\n\t\t\t// on every exit path -- including a spawn/wait rejection -- or a crashed launch\n\t\t\t// leaks an open handle onto a host-owned credential file for the process's life.\n\t\t\t// The workspace is bind-mounted read-write into the sandbox, so a file\n\t\t\t// here is visible at the same absolute path on both sides of it.\n\t\t\tconst terminalSizePath = join(stateDirectory, \"terminal-size\");\n\t\t\tconst stopPublishingTerminalSize = publishTerminalSize(terminalSizePath);\n\n\t\t\t// The second child derives its argv from the same builder as the first, with one\n\t\t\t// extra writable root. Nothing about the original child's namespace changes.\n\t\t\tescalationProxy = await createCommandEscalationProxy({\n\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\tviolationStore,\n\t\t\t\trunEscalated: (request) =>\n\t\t\t\t\trunEscalatedCommand({\n\t\t\t\t\t\trequest,\n\t\t\t\t\t\tlaunch,\n\t\t\t\t\t\tstateDirectory,\n\t\t\t\t\t\tspawnBwrap: options?.spawnChild ?? spawn,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst spawnBwrap = options?.spawnChild ?? spawn;\n\t\t\t\tconst child = spawnBwrap(\n\t\t\t\t\t\"bwrap\",\n\t\t\t\t\tbuildBwrapArguments({\n\t\t\t\t\t\tworkspace: launch.policy.workspace,\n\t\t\t\t\t\tadditionalWritableRoots: launch.policy.additionalWritableRoots,\n\t\t\t\t\t\treadOnlyPaths: [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])],\n\t\t\t\t\t\treadOnlyFiles: launch.readOnlyFiles ?? [],\n\t\t\t\t\t\treadOnlyBinaries: launch.readOnlyBinaries ?? [],\n\t\t\t\t\t\tsockets: [\n\t\t\t\t\t\t\t{ hostPath: hostSocketPath, childPath: childSocketPath },\n\t\t\t\t\t\t\t// Its child-side path is what `APEX_CREDENTIAL_PROXY_PATH` names.\n\t\t\t\t\t\t\t...(launch.credentialChannel\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\thostPath: launch.credentialChannel.hostSocketPath,\n\t\t\t\t\t\t\t\t\t\t\tchildPath: launch.credentialChannel.childSocketPath,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\thostPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\t\t\t\tchildPath: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenvironment: {\n\t\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\tAPEX_UDS_PATH: childSocketPath,\n\t\t\t\t\t\t\t[TERMINAL_SIZE_PATH_VARIABLE]: terminalSizePath,\n\t\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: stateDirectory,\n\t\t\t\t\t\t\t[GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath,\n\t\t\t\t\t\t\t[COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tcommand: process.execPath,\n\t\t\t\t\t\targs: [relayScriptPath, launch.command, ...launch.args],\n\t\t\t\t\t}),\n\t\t\t\t\t{ env: launch.environment, stdio: [\"inherit\", \"inherit\", \"pipe\", ...readOnlyFileDescriptors] },\n\t\t\t\t);\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\t\tstderr += chunk;\n\t\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t\t});\n\t\t\t\tconst exitCode = await waitForExit(child);\n\t\t\t\tawait proxy?.close();\n\t\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\t\tif (kind) {\n\t\t\t\t\tviolationStore?.add({\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn exitCode;\n\t\t\t} finally {\n\t\t\t\tstopPublishingTerminalSize();\n\t\t\t\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t\t// The socket now lives outside the workspace, so nothing else will ever clean\n\t\t\t\t// it up; leaving it would litter the temp directory once per launch.\n\t\t\t\trmSync(hostSocketPath, { force: true });\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n\n/**\n * Start one approved command in its own child, with one extra writable root.\n *\n * Deliberately minimal compared with the session child: no network relay, no credential\n * channels, no terminal handoff. An escalated command is a single shell invocation the\n * human just read and approved, not a session, and every channel omitted here is one it\n * cannot reach. Its stdio is captured rather than inherited so the session that asked\n * receives the output instead of the escalated process writing over the TUI.\n */\nasync function runEscalatedCommand(options: {\n\trequest: CommandEscalationRequest;\n\tlaunch: SandboxLaunch;\n\tstateDirectory: string;\n\tspawnBwrap: typeof spawn;\n}): Promise<CommandEscalationResult> {\n\tconst { request, launch, stateDirectory } = options;\n\tconst child = options.spawnBwrap(\n\t\t\"bwrap\",\n\t\tbuildBwrapArguments({\n\t\t\tworkspace: launch.policy.workspace,\n\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\treadOnlyPaths: launch.readOnlyPaths ?? [],\n\t\t\treadOnlyFiles: [],\n\t\t\treadOnlyBinaries: [],\n\t\t\tsockets: [],\n\t\t\tenvironment: {\n\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t},\n\t\t\tcommand: \"/bin/sh\",\n\t\t\targs: [\"-c\", request.command],\n\t\t}),\n\t\t{ stdio: [\"ignore\", \"pipe\", \"pipe\"] },\n\t);\n\tlet stdout = \"\";\n\tlet stderr = \"\";\n\tchild.stdout?.setEncoding(\"utf8\");\n\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\tstdout += chunk;\n\t});\n\tchild.stderr?.setEncoding(\"utf8\");\n\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\tstderr += chunk;\n\t});\n\tconst code = await new Promise<number>((resolveCode) => {\n\t\tchild.once(\"error\", () => resolveCode(1));\n\t\tchild.once(\"exit\", (exitCode) => resolveCode(exitCode ?? 1));\n\t});\n\treturn { code, stdout, stderr };\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { spawn } from "node:child_process";
2
+ import { type CommandEscalationRequest } from "./rpc/command-proxy.ts";
3
+ import type { SandboxBackend } from "./supervisor.ts";
4
+ import type { SandboxViolationStore } from "./violations.ts";
5
+ export interface MacosSandboxBackendOptions {
6
+ /** Injectable only for preflight tests; production uses the running platform. */
7
+ platform?: NodeJS.Platform;
8
+ commandExists?: (command: string) => boolean;
9
+ violationStore?: SandboxViolationStore;
10
+ /**
11
+ * Injectable only so violation attribution can be tested off macOS; production
12
+ * spawns `sandbox-exec` directly. Without this the decision about what counts as a
13
+ * violation would be verifiable only on a macOS host.
14
+ */
15
+ spawnChild?: typeof spawn;
16
+ /** Injectable only for tests, which have no terminal to approve an escalation at. */
17
+ requestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;
18
+ }
19
+ /**
20
+ * macOS's platform adapter. Seatbelt (`sandbox-exec`) allows reads broadly except
21
+ * the invoking account's home directory, denies writes outside the workspace, and
22
+ * denies network outside the sandbox proxy's exact loopback port -- narrower than
23
+ * the wildcard `localhost:*` pattern some other sandboxes use. This mirrors Linux's
24
+ * actual posture (`--ro-bind / /` plus a hidden `/home`) rather than the narrower
25
+ * read-allowlist this design started with, which broke ordinary process startup.
26
+ */
27
+ export declare function createMacosSandboxBackend(options?: MacosSandboxBackendOptions): SandboxBackend;
28
+ //# sourceMappingURL=macos-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"macos-backend.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/macos-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAMtD,OAAO,EAEN,KAAK,wBAAwB,EAG7B,MAAM,wBAAwB,CAAC;AAQhC,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AAErE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,0BAA0B;IAC1C,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,KAAK,CAAC;IAC1B,qFAAqF;IACrF,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnF;AA6ED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CAqO9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { mkdirSync, realpathSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { createCommandEscalationApprover, createCredentialReleaser, createHostApprover } from \"./host-approval.ts\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport {\n\tCOMMAND_ESCALATION_SOCKET_VARIABLE,\n\ttype CommandEscalationRequest,\n\tcreateCommandEscalationProxy,\n\tresolveCommandEscalationChannelPaths,\n} from \"./rpc/command-proxy.ts\";\nimport {\n\tfillHostGitCredential,\n\tGIT_CREDENTIAL_SOCKET_VARIABLE,\n\tresolveGitCredentialChannelPaths,\n\twriteGitCredentialHelper,\n} from \"./rpc/git-credential-helper.ts\";\nimport { createGitCredentialProxy, type GitCredentialProxy } from \"./rpc/git-credential-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport { createTerminalHandoff, TERMINAL_HANDOFF_PATH_VARIABLE, type TerminalHandoff } from \"./terminal-handoff.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface MacosSandboxBackendOptions {\n\t/** Injectable only for preflight tests; production uses the running platform. */\n\tplatform?: NodeJS.Platform;\n\tcommandExists?: (command: string) => boolean;\n\tviolationStore?: SandboxViolationStore;\n\t/**\n\t * Injectable only so violation attribution can be tested off macOS; production\n\t * spawns `sandbox-exec` directly. Without this the decision about what counts as a\n\t * violation would be verifiable only on a macOS host.\n\t */\n\tspawnChild?: typeof spawn;\n\t/** Injectable only for tests, which have no terminal to approve an escalation at. */\n\trequestCommandEscalation?: (request: CommandEscalationRequest) => Promise<boolean>;\n}\n\nfunction sandboxExecExists(command: string): boolean {\n\t// sandbox-exec has no --version flag; a real, minimal profile run is the\n\t// standard existence-and-basic-functionality probe (see the 2b.5 prototype's\n\t// own positive control).\n\tconst result = spawnSync(command, [\"-p\", \"(version 1)(allow default)\", \"/usr/bin/true\"], {\n\t\tstdio: \"ignore\",\n\t\ttimeout: 2_000,\n\t});\n\treturn result.status === 0;\n}\n\nfunction waitForExit(child: ReturnType<typeof spawn>): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"exit\", (code) => resolve(code ?? 1));\n\t});\n}\n\n/**\n * Unlike Linux's bwrap (which needs each mount ancestor declared for its bind-mount\n * tree), Seatbelt's `subpath` is recursive from a single root, so only the directory\n * containing each path is needed -- no ancestor walk. Each directory is resolved to\n * its real (symlink-free) path: Seatbelt canonicalizes the path it actually checks\n * against a `subpath` rule (confirmed empirically -- a rule built from an\n * unresolved symlinked path, e.g. Homebrew's own layout, silently never matches).\n */\nfunction readOnlyDirectories(paths: readonly string[]): string[] {\n\tconst directories = paths.map((path) => dirname(resolve(path)));\n\tconst canonical = directories.map((directory) => {\n\t\ttry {\n\t\t\treturn realpathSync(directory);\n\t\t} catch {\n\t\t\treturn directory;\n\t\t}\n\t});\n\treturn [...new Set(canonical)];\n}\n\n/**\n * Canonicalize a channel socket path for a Seatbelt literal. Only the directory can be\n * resolved -- the socket file need not exist yet when the profile is written -- and the\n * same symlink rule as `readOnlyDirectories` applies: Seatbelt matches the canonical\n * path, so an unresolved one silently never matches.\n */\nfunction canonicalSocketPath(path: string): string {\n\tconst directory = dirname(resolve(path));\n\ttry {\n\t\treturn join(realpathSync(directory), basename(path));\n\t} catch {\n\t\treturn join(directory, basename(path));\n\t}\n}\n\n/** Seatbelt refuses through the blocked call's own errno text, not a wrapper message. */\nconst DENIAL_EVIDENCE = /Operation not permitted|Permission denied|EPERM|EACCES/i;\n\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | \"unknown\" | undefined {\n\t// Confirmed empirically (2b.5 prototype): unlike Linux's bwrap, macOS Seatbelt\n\t// denials surface through the launched command's own generic \"Operation not\n\t// permitted\"/EPERM text for filesystem AND network refusals alike -- there is no\n\t// macOS-specific phrase equivalent to Linux's distinct \"Network is unreachable\".\n\t// Only sandbox-exec's own wrapper-level exec refusal (a sign our own profile\n\t// forgot to allow a binary, not a policy decision) is unambiguous from stderr\n\t// text. Anything else that still shows a refusal stays \"unknown\";\n\t// network-proxy.ts's own precise per-request recording is the reliable \"network\"\n\t// signal on this platform.\n\tif (/execvp\\(\\) of .* failed: Operation not permitted/i.test(stderr)) return \"filesystem\";\n\tif (DENIAL_EVIDENCE.test(stderr)) return \"unknown\";\n\t// No refusal in evidence. The coarseness above is about telling filesystem from\n\t// network, not about telling a denial from an ordinary failure -- a non-zero exit\n\t// on its own says nothing about the boundary, and reporting it as a violation\n\t// sends the reader looking for a refusal that never happened.\n\treturn undefined;\n}\n\n/**\n * macOS's platform adapter. Seatbelt (`sandbox-exec`) allows reads broadly except\n * the invoking account's home directory, denies writes outside the workspace, and\n * denies network outside the sandbox proxy's exact loopback port -- narrower than\n * the wildcard `localhost:*` pattern some other sandboxes use. This mirrors Linux's\n * actual posture (`--ro-bind / /` plus a hidden `/home`) rather than the narrower\n * read-allowlist this design started with, which broke ordinary process startup.\n */\nexport function createMacosSandboxBackend(options?: MacosSandboxBackendOptions): SandboxBackend {\n\tconst platform = options?.platform ?? process.platform;\n\tif (platform !== \"darwin\") {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"OS sandbox is supported on macOS only.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"macOS sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\tconst hasCommand = options?.commandExists ?? sandboxExecExists;\n\tconst violationStore = options?.violationStore;\n\tlet proxy: SandboxNetworkProxy | undefined;\n\tlet handoff: TerminalHandoff | undefined;\n\tlet gitCredentialProxy: GitCredentialProxy | undefined;\n\tlet gitCredentialDirectory: string | undefined;\n\tlet escalationProxy: Awaited<ReturnType<typeof createCommandEscalationProxy>> | undefined;\n\tlet escalationDirectory: string | undefined;\n\n\tif (!hasCommand(\"sandbox-exec\")) {\n\t\treturn {\n\t\t\tstatus: { kind: \"unavailable\", reason: \"sandbox-exec (Seatbelt) is required for OS sandboxing.\" },\n\t\t\tasync launch() {\n\t\t\t\tthrow new Error(\"macOS sandbox backend is unavailable.\");\n\t\t\t},\n\t\t\tasync close() {},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: { kind: \"enforced\" },\n\t\tasync launch(launch: SandboxLaunch): Promise<number> {\n\t\t\t// Same reasoning as readOnlyDirectories: on macOS the workspace itself can\n\t\t\t// be reached through a symlink (e.g. os.tmpdir()'s /var -> /private/var),\n\t\t\t// and Seatbelt's subpath match is against the canonical path, not the one\n\t\t\t// the caller happened to spell. Resolve once, use everywhere below.\n\t\t\tconst workspace = realpathSync(launch.policy.workspace);\n\t\t\tconst writableRoots = launch.policy.additionalWritableRoots;\n\t\t\tconst stateDirectory = join(workspace, \".apex-code\", \"sandbox-state\");\n\t\t\tmkdirSync(stateDirectory, { recursive: true });\n\n\t\t\tconst violationCountBeforeLaunch = violationStore?.totalCount ?? 0;\n\n\t\t\thandoff = createTerminalHandoff(stateDirectory);\n\t\t\t// Both channels are AF_UNIX servers, which Windows has not got. Nothing is lost\n\t\t\t// by skipping them there: ADR 0005 leaves Windows unsupported and this backend\n\t\t\t// reports `unavailable` on any real Windows host. They are reachable here only\n\t\t\t// because the violation-attribution tests run this body off-macOS on purpose,\n\t\t\t// with `spawnChild` injected, and those must keep working on every runner.\n\t\t\tconst unixSocketsAvailable = process.platform !== \"win32\";\n\t\t\tconst gitCredentialPaths = unixSocketsAvailable ? resolveGitCredentialChannelPaths() : undefined;\n\t\t\tgitCredentialDirectory = gitCredentialPaths?.hostSocketDirectory;\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\ttcpHost: \"127.0.0.1\",\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t\t// Undefined without a terminal, which leaves the proxy's own\n\t\t\t\t// deny-without-asking path in place for headless, print, JSON, and RPC.\n\t\t\t\trequestApproval: createHostApprover({ handoff }),\n\t\t\t});\n\t\t\t// After the network proxy, because reachability is its answer to give.\n\t\t\tgitCredentialProxy =\n\t\t\t\tgitCredentialPaths &&\n\t\t\t\t(await createGitCredentialProxy({\n\t\t\t\t\tsocketPath: gitCredentialPaths.hostSocketPath,\n\t\t\t\t\tisHostAllowed: (host) => proxy?.isHostReachable(host) ?? false,\n\t\t\t\t\trequestRelease: createCredentialReleaser({ handoff }),\n\t\t\t\t\t// The supervisor's own environment: this runs outside the boundary and must\n\t\t\t\t\t// resolve against the real host home the child cannot see.\n\t\t\t\t\tfillCredential: (request) => fillHostGitCredential(request, { environment: process.env }),\n\t\t\t\t\tviolationStore,\n\t\t\t\t}));\n\t\t\twriteGitCredentialHelper(stateDirectory);\n\n\t\t\tconst escalationPaths = unixSocketsAvailable ? resolveCommandEscalationChannelPaths() : undefined;\n\t\t\tescalationDirectory = escalationPaths?.hostSocketDirectory;\n\t\t\t// Seatbelt cannot remap a path, so the child reaches this socket at its host\n\t\t\t// path, exactly as it reaches the credential channel.\n\t\t\tescalationProxy =\n\t\t\t\tescalationPaths &&\n\t\t\t\t(await createCommandEscalationProxy({\n\t\t\t\t\tsocketPath: escalationPaths.hostSocketPath,\n\t\t\t\t\trequestApproval: options?.requestCommandEscalation ?? createCommandEscalationApprover({ handoff }),\n\t\t\t\t\tviolationStore,\n\t\t\t\t\trunEscalated: async (request) => {\n\t\t\t\t\t\tconst escalated = createMacosSandboxBackend(options);\n\t\t\t\t\t\tconst code = await escalated.launch({\n\t\t\t\t\t\t\t...launch,\n\t\t\t\t\t\t\tcommand: \"/bin/sh\",\n\t\t\t\t\t\t\targs: [\"-c\", request.command],\n\t\t\t\t\t\t\tpolicy: {\n\t\t\t\t\t\t\t\t...launch.policy,\n\t\t\t\t\t\t\t\tadditionalWritableRoots: [...launch.policy.additionalWritableRoots, request.writableRoot],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t\tawait escalated.close();\n\t\t\t\t\t\treturn { code, stdout: \"\", stderr: \"\" };\n\t\t\t\t\t},\n\t\t\t\t}));\n\t\t\tconst proxyPort = proxy.port as number;\n\n\t\t\t// Seatbelt cannot remap a path, so the child connects to the channel socket at\n\t\t\t// its host path, and the profile allows outbound to exactly that one socket.\n\t\t\tconst credentialChannelSocket = launch.credentialChannel\n\t\t\t\t? canonicalSocketPath(launch.credentialChannel.hostSocketPath)\n\t\t\t\t: undefined;\n\n\t\t\tconst readOnlyDirs = readOnlyDirectories([process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])]);\n\t\t\tconst readOnlyFiles = (launch.readOnlyFiles ?? []).map((path) => {\n\t\t\t\ttry {\n\t\t\t\t\treturn realpathSync(path);\n\t\t\t\t} catch {\n\t\t\t\t\treturn resolve(path);\n\t\t\t\t}\n\t\t\t});\n\t\t\tlet userHome: string | undefined;\n\t\t\ttry {\n\t\t\t\tuserHome = realpathSync(homedir());\n\t\t\t} catch {\n\t\t\t\tuserHome = homedir();\n\t\t\t}\n\n\t\t\tconst profileLines = [\n\t\t\t\t\"(version 1)\",\n\t\t\t\t// bsd.sb's baseline is broader than Linux's opt-in bind-mount model --\n\t\t\t\t// a real gap this design accepts, documented in the spec's fourth\n\t\t\t\t// amendment -- but it is the only combination the 2b.5 prototype found\n\t\t\t\t// that lets a normal Unix child process (dyld, /bin/sh) actually start.\n\t\t\t\t'(import \"bsd.sb\")',\n\t\t\t\t\"(allow process-exec*)\",\n\t\t\t\t// Separate from process-exec* -- confirmed empirically: a shell that\n\t\t\t\t// forks a child shell (e.g. `sh -c 'a' && sh -c 'b'`) failed with\n\t\t\t\t// \"fork: Operation not permitted\" without this, even with exec allowed.\n\t\t\t\t\"(allow process-fork)\",\n\t\t\t\t// Matches Linux's actual posture (bwrap's `--ro-bind / /`): broad read,\n\t\t\t\t// narrow write. A read-only allowlist alone (this design's first cut)\n\t\t\t\t// breaks ordinary process startup -- confirmed empirically, getcwd()\n\t\t\t\t// itself needs to traverse the workspace's ancestor directories, which\n\t\t\t\t// no read rule scoped only to the workspace or runtime paths covers.\n\t\t\t\t\"(allow file-read*)\",\n\t\t\t\t'(deny file-read* (subpath (param \"USER_HOME\")))',\n\t\t\t\t...readOnlyDirs.map((_, index) => `(allow file-read* (subpath (param \"RO_${index}\")))`),\n\t\t\t\t...readOnlyFiles.map((_, index) => `(allow file-read* (subpath (param \"RO_FILE_${index}\")))`),\n\t\t\t\t'(allow file-read* (subpath (param \"WORKSPACE\")))',\n\t\t\t\t\"(deny file-write*)\",\n\t\t\t\t'(allow file-write* (subpath (param \"WORKSPACE\")))',\n\t\t\t\t// Each extra root is allowed exactly as the workspace is, and only ever from\n\t\t\t\t// an argv-parsed flag: a repository that could name its own writable root\n\t\t\t\t// would be granting itself authority (ADR 0016).\n\t\t\t\t...writableRoots.map((_, index) => `(allow file-read* (subpath (param \"RW_${index}\")))`),\n\t\t\t\t...writableRoots.map((_, index) => `(allow file-write* (subpath (param \"RW_${index}\")))`),\n\t\t\t\t\"(deny network*)\",\n\t\t\t\t'(allow network-outbound (remote ip (param \"PROXY_ADDR\")))',\n\t\t\t\t...(credentialChannelSocket\n\t\t\t\t\t? [`(allow network-outbound (remote unix-socket (literal \"${credentialChannelSocket}\")))`]\n\t\t\t\t\t: []),\n\t\t\t\t...(gitCredentialPaths\n\t\t\t\t\t? [`(allow network-outbound (remote unix-socket (literal \"${gitCredentialPaths.hostSocketPath}\")))`]\n\t\t\t\t\t: []),\n\t\t\t\t...(escalationPaths\n\t\t\t\t\t? [`(allow network-outbound (remote unix-socket (literal \"${escalationPaths.hostSocketPath}\")))`]\n\t\t\t\t\t: []),\n\t\t\t];\n\t\t\tconst profilePath = join(stateDirectory, \"profile.sb\");\n\t\t\twriteFileSync(profilePath, profileLines.join(\"\\n\"));\n\n\t\t\tconst params: string[] = [\"-D\", `USER_HOME=${userHome}`];\n\t\t\treadOnlyDirs.forEach((dir, index) => {\n\t\t\t\tparams.push(\"-D\", `RO_${index}=${dir}`);\n\t\t\t});\n\t\t\treadOnlyFiles.forEach((file, index) => {\n\t\t\t\tparams.push(\"-D\", `RO_FILE_${index}=${file}`);\n\t\t\t});\n\t\t\tparams.push(\"-D\", `WORKSPACE=${workspace}`);\n\t\t\twritableRoots.forEach((root, index) => {\n\t\t\t\tparams.push(\"-D\", `RW_${index}=${root}`);\n\t\t\t});\n\t\t\tparams.push(\"-D\", `PROXY_ADDR=localhost:${proxyPort}`);\n\n\t\t\tconst spawnSandboxExec = options?.spawnChild ?? spawn;\n\t\t\tconst child = spawnSandboxExec(\n\t\t\t\t\"sandbox-exec\",\n\t\t\t\t[\"-f\", profilePath, ...params, \"--\", launch.command, ...launch.args],\n\t\t\t\t{\n\t\t\t\t\tcwd: workspace,\n\t\t\t\t\tenv: {\n\t\t\t\t\t\t...launch.environment,\n\t\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\tHTTP_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t\t\tHTTPS_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t\t\t[TERMINAL_HANDOFF_PATH_VARIABLE]: stateDirectory,\n\t\t\t\t\t\t...(gitCredentialPaths\n\t\t\t\t\t\t\t? { [GIT_CREDENTIAL_SOCKET_VARIABLE]: gitCredentialPaths.childSocketPath }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t...(escalationPaths ? { [COMMAND_ESCALATION_SOCKET_VARIABLE]: escalationPaths.childSocketPath } : {}),\n\t\t\t\t\t},\n\t\t\t\t\tstdio: [\"inherit\", \"inherit\", \"pipe\"],\n\t\t\t\t},\n\t\t\t);\n\t\t\tlet stderr = \"\";\n\t\t\tchild.stderr?.setEncoding(\"utf8\");\n\t\t\tchild.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t\tstderr += chunk;\n\t\t\t\tprocess.stderr.write(chunk);\n\t\t\t});\n\t\t\tconst exitCode = await waitForExit(child);\n\t\t\tawait proxy?.close();\n\t\t\tconst proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;\n\t\t\tconst kind = exitCode !== 0 && !proxyAlreadyRecordedAViolation ? classifySandboxFailure(stderr) : undefined;\n\t\t\tif (kind) {\n\t\t\t\tviolationStore?.add({\n\t\t\t\t\tkind,\n\t\t\t\t\tcommand: [launch.command, ...launch.args].join(\" \"),\n\t\t\t\t\tdetail: stderr.trim(),\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn exitCode;\n\t\t},\n\t\tasync close() {\n\t\t\thandoff?.stop();\n\t\t\tawait escalationProxy?.close();\n\t\t\tif (escalationDirectory) rmSync(escalationDirectory, { force: true, recursive: true });\n\t\t\tawait gitCredentialProxy?.close();\n\t\t\tif (gitCredentialDirectory) rmSync(gitCredentialDirectory, { force: true, recursive: true });\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}