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

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 (426) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +66 -681
  3. package/dist/cli/args.d.ts +5 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +45 -6
  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/cost-command.d.ts +19 -0
  11. package/dist/cli/cost-command.d.ts.map +1 -0
  12. package/dist/cli/cost-command.js +107 -0
  13. package/dist/cli/cost-command.js.map +1 -0
  14. package/dist/cli/startup-ui.d.ts +1 -1
  15. package/dist/cli/startup-ui.d.ts.map +1 -1
  16. package/dist/cli/startup-ui.js +1 -2
  17. package/dist/cli/startup-ui.js.map +1 -1
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +45 -16
  20. package/dist/cli.js.map +1 -1
  21. package/dist/config.d.ts +2 -2
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +14 -7
  24. package/dist/config.js.map +1 -1
  25. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  26. package/dist/core/agent-session-runtime.js +2 -0
  27. package/dist/core/agent-session-runtime.js.map +1 -1
  28. package/dist/core/agent-session-services.d.ts +9 -0
  29. package/dist/core/agent-session-services.d.ts.map +1 -1
  30. package/dist/core/agent-session-services.js +30 -4
  31. package/dist/core/agent-session-services.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +46 -0
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +137 -19
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/auth-storage.d.ts.map +1 -1
  37. package/dist/core/auth-storage.js +6 -6
  38. package/dist/core/auth-storage.js.map +1 -1
  39. package/dist/core/bash-executor.d.ts.map +1 -1
  40. package/dist/core/bash-executor.js +1 -1
  41. package/dist/core/bash-executor.js.map +1 -1
  42. package/dist/core/context/deferred-schemas.d.ts +92 -0
  43. package/dist/core/context/deferred-schemas.d.ts.map +1 -0
  44. package/dist/core/context/deferred-schemas.js +74 -0
  45. package/dist/core/context/deferred-schemas.js.map +1 -0
  46. package/dist/core/context/eviction.d.ts +47 -0
  47. package/dist/core/context/eviction.d.ts.map +1 -0
  48. package/dist/core/context/eviction.js +83 -0
  49. package/dist/core/context/eviction.js.map +1 -0
  50. package/dist/core/context/pipeline.d.ts +90 -0
  51. package/dist/core/context/pipeline.d.ts.map +1 -0
  52. package/dist/core/context/pipeline.js +109 -0
  53. package/dist/core/context/pipeline.js.map +1 -0
  54. package/dist/core/credential-failover.d.ts +30 -0
  55. package/dist/core/credential-failover.d.ts.map +1 -0
  56. package/dist/core/credential-failover.js +46 -0
  57. package/dist/core/credential-failover.js.map +1 -0
  58. package/dist/core/credential-pool.d.ts +60 -0
  59. package/dist/core/credential-pool.d.ts.map +1 -0
  60. package/dist/core/credential-pool.js +84 -0
  61. package/dist/core/credential-pool.js.map +1 -0
  62. package/dist/core/delegation/agents.d.ts +15 -0
  63. package/dist/core/delegation/agents.d.ts.map +1 -0
  64. package/dist/core/delegation/agents.js +55 -0
  65. package/dist/core/delegation/agents.js.map +1 -0
  66. package/dist/core/delegation/ceiling.d.ts +29 -0
  67. package/dist/core/delegation/ceiling.d.ts.map +1 -0
  68. package/dist/core/delegation/ceiling.js +34 -0
  69. package/dist/core/delegation/ceiling.js.map +1 -0
  70. package/dist/core/delegation/runtime.d.ts +94 -0
  71. package/dist/core/delegation/runtime.d.ts.map +1 -0
  72. package/dist/core/delegation/runtime.js +117 -0
  73. package/dist/core/delegation/runtime.js.map +1 -0
  74. package/dist/core/durable-state/daemon.d.ts +37 -0
  75. package/dist/core/durable-state/daemon.d.ts.map +1 -0
  76. package/dist/core/durable-state/daemon.js +62 -0
  77. package/dist/core/durable-state/daemon.js.map +1 -0
  78. package/dist/core/durable-state/provenance.d.ts +7 -0
  79. package/dist/core/durable-state/provenance.d.ts.map +1 -0
  80. package/dist/core/durable-state/provenance.js +24 -0
  81. package/dist/core/durable-state/provenance.js.map +1 -0
  82. package/dist/core/durable-state/sqlite.d.ts +84 -0
  83. package/dist/core/durable-state/sqlite.d.ts.map +1 -0
  84. package/dist/core/durable-state/sqlite.js +331 -0
  85. package/dist/core/durable-state/sqlite.js.map +1 -0
  86. package/dist/core/environment.d.ts +12 -0
  87. package/dist/core/environment.d.ts.map +1 -0
  88. package/dist/core/environment.js +69 -0
  89. package/dist/core/environment.js.map +1 -0
  90. package/dist/core/evidence-policy.d.ts +17 -0
  91. package/dist/core/evidence-policy.d.ts.map +1 -0
  92. package/dist/core/evidence-policy.js +25 -0
  93. package/dist/core/evidence-policy.js.map +1 -0
  94. package/dist/core/evidence.d.ts +16 -0
  95. package/dist/core/evidence.d.ts.map +1 -0
  96. package/dist/core/evidence.js +51 -0
  97. package/dist/core/evidence.js.map +1 -0
  98. package/dist/core/experimental.d.ts.map +1 -1
  99. package/dist/core/experimental.js +2 -1
  100. package/dist/core/experimental.js.map +1 -1
  101. package/dist/core/extensions/types.d.ts +2 -0
  102. package/dist/core/extensions/types.d.ts.map +1 -1
  103. package/dist/core/extensions/types.js.map +1 -1
  104. package/dist/core/index.d.ts +1 -0
  105. package/dist/core/index.d.ts.map +1 -1
  106. package/dist/core/index.js +1 -0
  107. package/dist/core/index.js.map +1 -1
  108. package/dist/core/model-config.d.ts +8 -0
  109. package/dist/core/model-config.d.ts.map +1 -1
  110. package/dist/core/model-config.js +21 -5
  111. package/dist/core/model-config.js.map +1 -1
  112. package/dist/core/model-resolver.d.ts +20 -0
  113. package/dist/core/model-resolver.d.ts.map +1 -1
  114. package/dist/core/model-resolver.js +59 -0
  115. package/dist/core/model-resolver.js.map +1 -1
  116. package/dist/core/model-runtime.d.ts +73 -0
  117. package/dist/core/model-runtime.d.ts.map +1 -1
  118. package/dist/core/model-runtime.js +225 -8
  119. package/dist/core/model-runtime.js.map +1 -1
  120. package/dist/core/observability/aggregate.d.ts +21 -0
  121. package/dist/core/observability/aggregate.d.ts.map +1 -0
  122. package/dist/core/observability/aggregate.js +36 -0
  123. package/dist/core/observability/aggregate.js.map +1 -0
  124. package/dist/core/observability/otlp.d.ts +66 -0
  125. package/dist/core/observability/otlp.d.ts.map +1 -0
  126. package/dist/core/observability/otlp.js +95 -0
  127. package/dist/core/observability/otlp.js.map +1 -0
  128. package/dist/core/package-manager.d.ts.map +1 -1
  129. package/dist/core/package-manager.js +3 -2
  130. package/dist/core/package-manager.js.map +1 -1
  131. package/dist/core/permissions/gate.d.ts +35 -0
  132. package/dist/core/permissions/gate.d.ts.map +1 -0
  133. package/dist/core/permissions/gate.js +72 -0
  134. package/dist/core/permissions/gate.js.map +1 -0
  135. package/dist/core/permissions/modes.d.ts +18 -0
  136. package/dist/core/permissions/modes.d.ts.map +1 -0
  137. package/dist/core/permissions/modes.js +57 -0
  138. package/dist/core/permissions/modes.js.map +1 -0
  139. package/dist/core/permissions/responder.d.ts +30 -0
  140. package/dist/core/permissions/responder.d.ts.map +1 -0
  141. package/dist/core/permissions/responder.js +32 -0
  142. package/dist/core/permissions/responder.js.map +1 -0
  143. package/dist/core/permissions/rules.d.ts +34 -0
  144. package/dist/core/permissions/rules.d.ts.map +1 -0
  145. package/dist/core/permissions/rules.js +74 -0
  146. package/dist/core/permissions/rules.js.map +1 -0
  147. package/dist/core/permissions/startup.d.ts +33 -0
  148. package/dist/core/permissions/startup.d.ts.map +1 -0
  149. package/dist/core/permissions/startup.js +55 -0
  150. package/dist/core/permissions/startup.js.map +1 -0
  151. package/dist/core/permissions/store.d.ts +114 -0
  152. package/dist/core/permissions/store.d.ts.map +1 -0
  153. package/dist/core/permissions/store.js +229 -0
  154. package/dist/core/permissions/store.js.map +1 -0
  155. package/dist/core/project-trust.d.ts.map +1 -1
  156. package/dist/core/project-trust.js +1 -1
  157. package/dist/core/project-trust.js.map +1 -1
  158. package/dist/core/provider-attribution.d.ts.map +1 -1
  159. package/dist/core/provider-attribution.js +6 -7
  160. package/dist/core/provider-attribution.js.map +1 -1
  161. package/dist/core/remote-catalog-provider.d.ts +2 -2
  162. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  163. package/dist/core/remote-catalog-provider.js +20 -16
  164. package/dist/core/remote-catalog-provider.js.map +1 -1
  165. package/dist/core/sandbox/child-entry.d.ts +2 -0
  166. package/dist/core/sandbox/child-entry.d.ts.map +1 -0
  167. package/dist/core/sandbox/child-entry.js +11 -0
  168. package/dist/core/sandbox/child-entry.js.map +1 -0
  169. package/dist/core/sandbox/cli-launch.d.ts +27 -0
  170. package/dist/core/sandbox/cli-launch.d.ts.map +1 -0
  171. package/dist/core/sandbox/cli-launch.js +133 -0
  172. package/dist/core/sandbox/cli-launch.js.map +1 -0
  173. package/dist/core/sandbox/cli-supervisor.d.ts +25 -0
  174. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -0
  175. package/dist/core/sandbox/cli-supervisor.js +57 -0
  176. package/dist/core/sandbox/cli-supervisor.js.map +1 -0
  177. package/dist/core/sandbox/linux-backend.d.ts +18 -0
  178. package/dist/core/sandbox/linux-backend.d.ts.map +1 -0
  179. package/dist/core/sandbox/linux-backend.js +186 -0
  180. package/dist/core/sandbox/linux-backend.js.map +1 -0
  181. package/dist/core/sandbox/macos-backend.d.ts +18 -0
  182. package/dist/core/sandbox/macos-backend.d.ts.map +1 -0
  183. package/dist/core/sandbox/macos-backend.js +195 -0
  184. package/dist/core/sandbox/macos-backend.js.map +1 -0
  185. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  186. package/dist/core/sandbox/network-proxy.d.ts.map +1 -0
  187. package/dist/core/sandbox/network-proxy.js +79 -0
  188. package/dist/core/sandbox/network-proxy.js.map +1 -0
  189. package/dist/core/sandbox/policy.d.ts +32 -0
  190. package/dist/core/sandbox/policy.d.ts.map +1 -0
  191. package/dist/core/sandbox/policy.js +38 -0
  192. package/dist/core/sandbox/policy.js.map +1 -0
  193. package/dist/core/sandbox/supervisor.d.ts +31 -0
  194. package/dist/core/sandbox/supervisor.d.ts.map +1 -0
  195. package/dist/core/sandbox/supervisor.js +18 -0
  196. package/dist/core/sandbox/supervisor.js.map +1 -0
  197. package/dist/core/sandbox/violations.d.ts +23 -0
  198. package/dist/core/sandbox/violations.d.ts.map +1 -0
  199. package/dist/core/sandbox/violations.js +30 -0
  200. package/dist/core/sandbox/violations.js.map +1 -0
  201. package/dist/core/sdk.d.ts +20 -36
  202. package/dist/core/sdk.d.ts.map +1 -1
  203. package/dist/core/sdk.js +129 -2
  204. package/dist/core/sdk.js.map +1 -1
  205. package/dist/core/session-manager.d.ts +41 -2
  206. package/dist/core/session-manager.d.ts.map +1 -1
  207. package/dist/core/session-manager.js +57 -0
  208. package/dist/core/session-manager.js.map +1 -1
  209. package/dist/core/session-share.d.ts +16 -0
  210. package/dist/core/session-share.d.ts.map +1 -0
  211. package/dist/core/session-share.js +22 -0
  212. package/dist/core/session-share.js.map +1 -0
  213. package/dist/core/settings-manager.d.ts +38 -9
  214. package/dist/core/settings-manager.d.ts.map +1 -1
  215. package/dist/core/settings-manager.js +39 -24
  216. package/dist/core/settings-manager.js.map +1 -1
  217. package/dist/core/system-prompt.d.ts.map +1 -1
  218. package/dist/core/system-prompt.js +5 -5
  219. package/dist/core/system-prompt.js.map +1 -1
  220. package/dist/core/timings.d.ts +1 -1
  221. package/dist/core/timings.d.ts.map +1 -1
  222. package/dist/core/timings.js +3 -2
  223. package/dist/core/timings.js.map +1 -1
  224. package/dist/core/tools/ask-user.d.ts +25 -0
  225. package/dist/core/tools/ask-user.d.ts.map +1 -0
  226. package/dist/core/tools/ask-user.js +49 -0
  227. package/dist/core/tools/ask-user.js.map +1 -0
  228. package/dist/core/tools/bash-command-segments.d.ts +22 -0
  229. package/dist/core/tools/bash-command-segments.d.ts.map +1 -0
  230. package/dist/core/tools/bash-command-segments.js +128 -0
  231. package/dist/core/tools/bash-command-segments.js.map +1 -0
  232. package/dist/core/tools/bash.d.ts +26 -3
  233. package/dist/core/tools/bash.d.ts.map +1 -1
  234. package/dist/core/tools/bash.js +105 -16
  235. package/dist/core/tools/bash.js.map +1 -1
  236. package/dist/core/tools/contract.d.ts +156 -0
  237. package/dist/core/tools/contract.d.ts.map +1 -0
  238. package/dist/core/tools/contract.js +48 -0
  239. package/dist/core/tools/contract.js.map +1 -0
  240. package/dist/core/tools/delegate.d.ts +38 -0
  241. package/dist/core/tools/delegate.d.ts.map +1 -0
  242. package/dist/core/tools/delegate.js +66 -0
  243. package/dist/core/tools/delegate.js.map +1 -0
  244. package/dist/core/tools/edit.d.ts +2 -2
  245. package/dist/core/tools/edit.d.ts.map +1 -1
  246. package/dist/core/tools/edit.js +24 -0
  247. package/dist/core/tools/edit.js.map +1 -1
  248. package/dist/core/tools/find.d.ts +2 -2
  249. package/dist/core/tools/find.d.ts.map +1 -1
  250. package/dist/core/tools/find.js +12 -0
  251. package/dist/core/tools/find.js.map +1 -1
  252. package/dist/core/tools/grep.d.ts +2 -2
  253. package/dist/core/tools/grep.d.ts.map +1 -1
  254. package/dist/core/tools/grep.js +12 -0
  255. package/dist/core/tools/grep.js.map +1 -1
  256. package/dist/core/tools/index.d.ts +29 -4
  257. package/dist/core/tools/index.d.ts.map +1 -1
  258. package/dist/core/tools/index.js +118 -2
  259. package/dist/core/tools/index.js.map +1 -1
  260. package/dist/core/tools/ls.d.ts +2 -2
  261. package/dist/core/tools/ls.d.ts.map +1 -1
  262. package/dist/core/tools/ls.js +12 -0
  263. package/dist/core/tools/ls.js.map +1 -1
  264. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  265. package/dist/core/tools/output-accumulator.js +1 -1
  266. package/dist/core/tools/output-accumulator.js.map +1 -1
  267. package/dist/core/tools/path-permission.d.ts +18 -0
  268. package/dist/core/tools/path-permission.d.ts.map +1 -0
  269. package/dist/core/tools/path-permission.js +28 -0
  270. package/dist/core/tools/path-permission.js.map +1 -0
  271. package/dist/core/tools/plan-present.d.ts +27 -0
  272. package/dist/core/tools/plan-present.d.ts.map +1 -0
  273. package/dist/core/tools/plan-present.js +55 -0
  274. package/dist/core/tools/plan-present.js.map +1 -0
  275. package/dist/core/tools/read.d.ts +2 -2
  276. package/dist/core/tools/read.d.ts.map +1 -1
  277. package/dist/core/tools/read.js +12 -0
  278. package/dist/core/tools/read.js.map +1 -1
  279. package/dist/core/tools/test.d.ts +34 -0
  280. package/dist/core/tools/test.d.ts.map +1 -0
  281. package/dist/core/tools/test.js +75 -0
  282. package/dist/core/tools/test.js.map +1 -0
  283. package/dist/core/tools/todo-write.d.ts +31 -0
  284. package/dist/core/tools/todo-write.d.ts.map +1 -0
  285. package/dist/core/tools/todo-write.js +51 -0
  286. package/dist/core/tools/todo-write.js.map +1 -0
  287. package/dist/core/tools/tool-definition-wrapper.d.ts +13 -2
  288. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  289. package/dist/core/tools/tool-definition-wrapper.js +11 -1
  290. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  291. package/dist/core/tools/tool-schema.d.ts +22 -0
  292. package/dist/core/tools/tool-schema.d.ts.map +1 -0
  293. package/dist/core/tools/tool-schema.js +45 -0
  294. package/dist/core/tools/tool-schema.js.map +1 -0
  295. package/dist/core/tools/web-fetch.d.ts +26 -0
  296. package/dist/core/tools/web-fetch.d.ts.map +1 -0
  297. package/dist/core/tools/web-fetch.js +62 -0
  298. package/dist/core/tools/web-fetch.js.map +1 -0
  299. package/dist/core/tools/web-search.d.ts +26 -0
  300. package/dist/core/tools/web-search.d.ts.map +1 -0
  301. package/dist/core/tools/web-search.js +53 -0
  302. package/dist/core/tools/web-search.js.map +1 -0
  303. package/dist/core/tools/write.d.ts +6 -2
  304. package/dist/core/tools/write.d.ts.map +1 -1
  305. package/dist/core/tools/write.js +32 -1
  306. package/dist/core/tools/write.js.map +1 -1
  307. package/dist/core/usage-performance-store.d.ts +56 -0
  308. package/dist/core/usage-performance-store.d.ts.map +1 -0
  309. package/dist/core/usage-performance-store.js +85 -0
  310. package/dist/core/usage-performance-store.js.map +1 -0
  311. package/dist/index.d.ts +1 -0
  312. package/dist/index.d.ts.map +1 -1
  313. package/dist/index.js +1 -0
  314. package/dist/index.js.map +1 -1
  315. package/dist/main.d.ts.map +1 -1
  316. package/dist/main.js +48 -6
  317. package/dist/main.js.map +1 -1
  318. package/dist/migrations.d.ts.map +1 -1
  319. package/dist/migrations.js +2 -2
  320. package/dist/migrations.js.map +1 -1
  321. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  322. package/dist/modes/interactive/components/config-selector.js +9 -6
  323. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  324. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -4
  325. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  326. package/dist/modes/interactive/components/first-time-setup.js +11 -39
  327. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  328. package/dist/modes/interactive/components/footer.d.ts +8 -1
  329. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  330. package/dist/modes/interactive/components/footer.js +49 -20
  331. package/dist/modes/interactive/components/footer.js.map +1 -1
  332. package/dist/modes/interactive/components/settings-selector.d.ts +2 -2
  333. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  334. package/dist/modes/interactive/components/settings-selector.js +6 -6
  335. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  336. package/dist/modes/interactive/components/tool-execution.d.ts +2 -1
  337. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  338. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  339. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  340. package/dist/modes/interactive/external-editor.js +58 -9
  341. package/dist/modes/interactive/external-editor.js.map +1 -1
  342. package/dist/modes/interactive/interactive-mode.d.ts +2 -3
  343. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  344. package/dist/modes/interactive/interactive-mode.js +92 -104
  345. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  346. package/dist/modes/interactive/theme/theme-schema.json +2 -2
  347. package/dist/package-manager-cli.d.ts.map +1 -1
  348. package/dist/package-manager-cli.js +15 -10
  349. package/dist/package-manager-cli.js.map +1 -1
  350. package/dist/rpc-entry.d.ts.map +1 -1
  351. package/dist/rpc-entry.js +2 -1
  352. package/dist/rpc-entry.js.map +1 -1
  353. package/dist/server/create-harness.d.ts +1 -1
  354. package/dist/server/create-harness.d.ts.map +1 -1
  355. package/dist/server/create-harness.js +6 -5
  356. package/dist/server/create-harness.js.map +1 -1
  357. package/dist/testing/replay/metrics.d.ts +24 -0
  358. package/dist/testing/replay/metrics.d.ts.map +1 -0
  359. package/dist/testing/replay/metrics.js +35 -0
  360. package/dist/testing/replay/metrics.js.map +1 -0
  361. package/dist/testing/replay/recorded-provider.d.ts +10 -0
  362. package/dist/testing/replay/recorded-provider.d.ts.map +1 -0
  363. package/dist/testing/replay/recorded-provider.js +108 -0
  364. package/dist/testing/replay/recorded-provider.js.map +1 -0
  365. package/dist/testing/replay/runner.d.ts +56 -0
  366. package/dist/testing/replay/runner.d.ts.map +1 -0
  367. package/dist/testing/replay/runner.js +406 -0
  368. package/dist/testing/replay/runner.js.map +1 -0
  369. package/dist/utils/apex-code-user-agent.d.ts +2 -0
  370. package/dist/utils/apex-code-user-agent.d.ts.map +1 -0
  371. package/dist/utils/apex-code-user-agent.js +5 -0
  372. package/dist/utils/apex-code-user-agent.js.map +1 -0
  373. package/dist/utils/clipboard-image.d.ts.map +1 -1
  374. package/dist/utils/clipboard-image.js +1 -1
  375. package/dist/utils/clipboard-image.js.map +1 -1
  376. package/dist/utils/tools-manager.d.ts +32 -0
  377. package/dist/utils/tools-manager.d.ts.map +1 -1
  378. package/dist/utils/tools-manager.js +198 -100
  379. package/dist/utils/tools-manager.js.map +1 -1
  380. package/dist/utils/version-check.d.ts +5 -5
  381. package/dist/utils/version-check.d.ts.map +1 -1
  382. package/dist/utils/version-check.js +23 -14
  383. package/dist/utils/version-check.js.map +1 -1
  384. package/dist/utils/windows-self-update.d.ts.map +1 -1
  385. package/dist/utils/windows-self-update.js +1 -1
  386. package/dist/utils/windows-self-update.js.map +1 -1
  387. package/docs/compaction.md +21 -21
  388. package/docs/containerization.md +24 -21
  389. package/docs/custom-provider.md +9 -9
  390. package/docs/development.md +11 -11
  391. package/docs/environment-variables.md +36 -77
  392. package/docs/extensions.md +47 -47
  393. package/docs/index.md +12 -24
  394. package/docs/json.md +6 -6
  395. package/docs/keybindings.md +3 -3
  396. package/docs/llama-cpp.md +7 -7
  397. package/docs/models.md +11 -11
  398. package/docs/packages.md +46 -42
  399. package/docs/prompt-templates.md +3 -3
  400. package/docs/providers.md +10 -10
  401. package/docs/quickstart.md +49 -32
  402. package/docs/rpc.md +11 -11
  403. package/docs/sdk.md +4 -4
  404. package/docs/security.md +18 -18
  405. package/docs/session-format.md +9 -9
  406. package/docs/sessions.md +13 -13
  407. package/docs/settings.md +10 -14
  408. package/docs/shell-aliases.md +1 -1
  409. package/docs/skills.md +8 -8
  410. package/docs/terminal-setup.md +12 -12
  411. package/docs/termux.md +4 -4
  412. package/docs/themes.md +8 -8
  413. package/docs/tmux.md +4 -4
  414. package/docs/tui.md +3 -3
  415. package/docs/usage.md +54 -51
  416. package/docs/windows.md +1 -1
  417. package/npm-shrinkwrap.json +5 -5
  418. package/package.json +2 -3
  419. package/dist/core/telemetry.d.ts +0 -3
  420. package/dist/core/telemetry.d.ts.map +0 -1
  421. package/dist/core/telemetry.js +0 -9
  422. package/dist/core/telemetry.js.map +0 -1
  423. package/dist/utils/pi-user-agent.d.ts +0 -2
  424. package/dist/utils/pi-user-agent.d.ts.map +0 -1
  425. package/dist/utils/pi-user-agent.js +0 -5
  426. package/dist/utils/pi-user-agent.js.map +0 -1
@@ -0,0 +1,186 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { closeSync, mkdirSync, openSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createSandboxNetworkProxy } from "./network-proxy.js";
5
+ function commandExists(command) {
6
+ const result = spawnSync(command, ["--version"], { stdio: "ignore", timeout: 1_000 });
7
+ return result.status === 0;
8
+ }
9
+ function waitForExit(child) {
10
+ return new Promise((resolve, reject) => {
11
+ child.once("error", reject);
12
+ child.once("exit", (code) => resolve(code ?? 1));
13
+ });
14
+ }
15
+ function readOnlyMountArguments(path, kind = "directory", descriptor = 3) {
16
+ const target = resolve(path);
17
+ const directory = dirname(target);
18
+ const ancestors = [];
19
+ let current = directory;
20
+ while (current !== "/" && current !== "/home") {
21
+ ancestors.push(current);
22
+ current = dirname(current);
23
+ }
24
+ const parentArguments = ancestors.reverse().flatMap((ancestor) => ["--dir", ancestor]);
25
+ return kind === "file"
26
+ ? [...parentArguments, "--tmpfs", directory, "--perms", "0400", "--file", String(descriptor), target]
27
+ : [...parentArguments, "--ro-bind", directory, directory];
28
+ }
29
+ function classifySandboxFailure(stderr) {
30
+ if (/Read-only file system|Permission denied|Operation not permitted/i.test(stderr))
31
+ return "filesystem";
32
+ if (/Network is unreachable|ENETUNREACH|EHOSTUNREACH/i.test(stderr))
33
+ return "network";
34
+ return "unknown";
35
+ }
36
+ /**
37
+ * Linux's deliberately small platform adapter. The sandbox starts from a read-only
38
+ * host root and bind-mounts only the canonical workspace as writable. `--unshare-net`
39
+ * removes all direct network interfaces for the child and every descendant.
40
+ */
41
+ export function createLinuxSandboxBackend(options) {
42
+ const platform = options?.platform ?? process.platform;
43
+ if (platform !== "linux") {
44
+ return {
45
+ status: { kind: "unavailable", reason: "OS sandbox is supported on Linux only." },
46
+ async launch() {
47
+ throw new Error("Linux sandbox backend is unavailable.");
48
+ },
49
+ async close() { },
50
+ };
51
+ }
52
+ const hasCommand = options?.commandExists ?? commandExists;
53
+ const violationStore = options?.violationStore;
54
+ let proxy;
55
+ if (!hasCommand("bwrap")) {
56
+ return {
57
+ status: { kind: "unavailable", reason: "Bubblewrap (bwrap) is required for OS sandboxing." },
58
+ async launch() {
59
+ throw new Error("Linux sandbox backend is unavailable.");
60
+ },
61
+ async close() { },
62
+ };
63
+ }
64
+ return {
65
+ status: { kind: "enforced" },
66
+ async launch(launch) {
67
+ const stateDirectory = join(launch.policy.workspace, ".apex-code", "sandbox-state");
68
+ mkdirSync(stateDirectory, { recursive: true });
69
+ const violationCountBeforeLaunch = violationStore?.totalCount ?? 0;
70
+ const socketPath = join(stateDirectory, "proxy.sock");
71
+ proxy = await createSandboxNetworkProxy({
72
+ socketPath,
73
+ allowedHosts: launch.policy.allowedHosts,
74
+ violationStore,
75
+ });
76
+ const relayScriptPath = join(stateDirectory, "relay.js");
77
+ writeFileSync(relayScriptPath, `
78
+ const net = require("node:net");
79
+ const child_process = require("node:child_process");
80
+
81
+ const socketPath = process.env.APEX_UDS_PATH;
82
+ const server = net.createServer((c) => {
83
+ const client = net.connect(socketPath);
84
+ c.pipe(client).pipe(c);
85
+ c.on("error", () => {});
86
+ client.on("error", () => {});
87
+ });
88
+
89
+ server.listen(0, "127.0.0.1", () => {
90
+ const port = server.address().port;
91
+ const env = Object.assign({}, process.env, {
92
+ HTTP_PROXY: \`http://127.0.0.1:\${port}\`,
93
+ HTTPS_PROXY: \`http://127.0.0.1:\${port}\`
94
+ });
95
+ const args = process.argv.slice(2);
96
+ const child = child_process.spawn(args[0], args.slice(1), {
97
+ stdio: "inherit",
98
+ env: env,
99
+ });
100
+ child.on("exit", (code, signal) => {
101
+ process.exit(code ?? (signal ? 128 : 1));
102
+ });
103
+ });
104
+ server.on("error", (err) => {
105
+ console.error("Relay error:", err);
106
+ process.exit(1);
107
+ });
108
+ `.trim());
109
+ const readOnlyMounts = [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])].flatMap((path) => readOnlyMountArguments(path));
110
+ const readOnlyFileMounts = (launch.readOnlyFiles ?? []).flatMap((path, index) => readOnlyMountArguments(path, "file", index + 3));
111
+ const readOnlyFileDescriptors = (launch.readOnlyFiles ?? []).map((path) => openSync(path, "r"));
112
+ // The descriptors above are opened before the child spawns and must be closed
113
+ // on every exit path -- including a spawn/wait rejection -- or a crashed launch
114
+ // leaks an open handle onto a host-owned credential file for the process's life.
115
+ try {
116
+ const spawnBwrap = options?.spawnChild ?? spawn;
117
+ const child = spawnBwrap("bwrap", [
118
+ "--new-session",
119
+ "--die-with-parent",
120
+ "--unshare-user",
121
+ "--unshare-pid",
122
+ "--unshare-net",
123
+ "--ro-bind",
124
+ "/",
125
+ "/",
126
+ "--tmpfs",
127
+ "/home",
128
+ ...readOnlyMounts,
129
+ ...readOnlyFileMounts,
130
+ "--bind",
131
+ launch.policy.workspace,
132
+ launch.policy.workspace,
133
+ "--dev",
134
+ "/dev",
135
+ "--proc",
136
+ "/proc",
137
+ "--chdir",
138
+ launch.policy.workspace,
139
+ "--setenv",
140
+ "HOME",
141
+ launch.environment?.HOME ?? stateDirectory,
142
+ "--setenv",
143
+ "TMPDIR",
144
+ launch.environment?.TMPDIR ?? stateDirectory,
145
+ "--setenv",
146
+ "APEX_UDS_PATH",
147
+ socketPath,
148
+ "--",
149
+ process.execPath,
150
+ relayScriptPath,
151
+ launch.command,
152
+ ...launch.args,
153
+ ], { env: launch.environment, stdio: ["inherit", "inherit", "pipe", ...readOnlyFileDescriptors] });
154
+ let stderr = "";
155
+ child.stderr?.setEncoding("utf8");
156
+ child.stderr?.on("data", (chunk) => {
157
+ stderr += chunk;
158
+ process.stderr.write(chunk);
159
+ });
160
+ const exitCode = await waitForExit(child);
161
+ await proxy?.close();
162
+ const proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;
163
+ if (exitCode !== 0 && !proxyAlreadyRecordedAViolation) {
164
+ const kind = classifySandboxFailure(stderr);
165
+ violationStore?.add({
166
+ kind,
167
+ command: [launch.command, ...launch.args].join(" "),
168
+ detail: kind === "unknown"
169
+ ? "Sandboxed process exited unsuccessfully; inspect its stderr for the OS refusal."
170
+ : stderr.trim(),
171
+ timestamp: new Date(),
172
+ });
173
+ }
174
+ return exitCode;
175
+ }
176
+ finally {
177
+ for (const descriptor of readOnlyFileDescriptors)
178
+ closeSync(descriptor);
179
+ }
180
+ },
181
+ async close() {
182
+ await proxy?.close();
183
+ },
184
+ };
185
+ }
186
+ //# 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 { closeSync, mkdirSync, openSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.ts\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\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}\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\nfunction readOnlyMountArguments(path: string, kind: \"directory\" | \"file\" = \"directory\", descriptor = 3): string[] {\n\tconst target = resolve(path);\n\tconst directory = dirname(target);\n\tconst ancestors: string[] = [];\n\tlet current = directory;\n\twhile (current !== \"/\" && current !== \"/home\") {\n\t\tancestors.push(current);\n\t\tcurrent = dirname(current);\n\t}\n\tconst parentArguments = ancestors.reverse().flatMap((ancestor) => [\"--dir\", ancestor]);\n\treturn kind === \"file\"\n\t\t? [...parentArguments, \"--tmpfs\", directory, \"--perms\", \"0400\", \"--file\", String(descriptor), target]\n\t\t: [...parentArguments, \"--ro-bind\", directory, directory];\n}\n\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | \"unknown\" {\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 \"unknown\";\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\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 socketPath = join(stateDirectory, \"proxy.sock\");\n\t\t\tproxy = await createSandboxNetworkProxy({\n\t\t\t\tsocketPath,\n\t\t\t\tallowedHosts: launch.policy.allowedHosts,\n\t\t\t\tviolationStore,\n\t\t\t});\n\n\t\t\tconst relayScriptPath = join(stateDirectory, \"relay.js\");\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 readOnlyMounts = [process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])].flatMap((path) =>\n\t\t\t\treadOnlyMountArguments(path),\n\t\t\t);\n\t\t\tconst readOnlyFileMounts = (launch.readOnlyFiles ?? []).flatMap((path, index) =>\n\t\t\t\treadOnlyMountArguments(path, \"file\", index + 3),\n\t\t\t);\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\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\t[\n\t\t\t\t\t\t\"--new-session\",\n\t\t\t\t\t\t\"--die-with-parent\",\n\t\t\t\t\t\t\"--unshare-user\",\n\t\t\t\t\t\t\"--unshare-pid\",\n\t\t\t\t\t\t\"--unshare-net\",\n\t\t\t\t\t\t\"--ro-bind\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"/\",\n\t\t\t\t\t\t\"--tmpfs\",\n\t\t\t\t\t\t\"/home\",\n\t\t\t\t\t\t...readOnlyMounts,\n\t\t\t\t\t\t...readOnlyFileMounts,\n\t\t\t\t\t\t\"--bind\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t\"--dev\",\n\t\t\t\t\t\t\"/dev\",\n\t\t\t\t\t\t\"--proc\",\n\t\t\t\t\t\t\"/proc\",\n\t\t\t\t\t\t\"--chdir\",\n\t\t\t\t\t\tlaunch.policy.workspace,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"HOME\",\n\t\t\t\t\t\tlaunch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"TMPDIR\",\n\t\t\t\t\t\tlaunch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\t\t\"--setenv\",\n\t\t\t\t\t\t\"APEX_UDS_PATH\",\n\t\t\t\t\t\tsocketPath,\n\t\t\t\t\t\t\"--\",\n\t\t\t\t\t\tprocess.execPath,\n\t\t\t\t\t\trelayScriptPath,\n\t\t\t\t\t\tlaunch.command,\n\t\t\t\t\t\t...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\tif (exitCode !== 0 && !proxyAlreadyRecordedAViolation) {\n\t\t\t\t\tconst kind = classifySandboxFailure(stderr);\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:\n\t\t\t\t\t\t\tkind === \"unknown\"\n\t\t\t\t\t\t\t\t? \"Sandboxed process exited unsuccessfully; inspect its stderr for the OS refusal.\"\n\t\t\t\t\t\t\t\t: 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\tfor (const descriptor of readOnlyFileDescriptors) closeSync(descriptor);\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { SandboxBackend } from "./supervisor.ts";
2
+ import type { SandboxViolationStore } from "./violations.ts";
3
+ export interface MacosSandboxBackendOptions {
4
+ /** Injectable only for preflight tests; production uses the running platform. */
5
+ platform?: NodeJS.Platform;
6
+ commandExists?: (command: string) => boolean;
7
+ violationStore?: SandboxViolationStore;
8
+ }
9
+ /**
10
+ * macOS's platform adapter. Seatbelt (`sandbox-exec`) allows reads broadly except
11
+ * the invoking account's home directory, denies writes outside the workspace, and
12
+ * denies network outside the sandbox proxy's exact loopback port -- narrower than
13
+ * the wildcard `localhost:*` pattern some other sandboxes use. This mirrors Linux's
14
+ * actual posture (`--ro-bind / /` plus a hidden `/home`) rather than the narrower
15
+ * read-allowlist this design started with, which broke ordinary process startup.
16
+ */
17
+ export declare function createMacosSandboxBackend(options?: MacosSandboxBackendOptions): SandboxBackend;
18
+ //# 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":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AACrE,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;CACvC;AAqDD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,cAAc,CAyI9F","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { mkdirSync, realpathSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.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}\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\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | \"unknown\" {\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 stays \"unknown\"; network-proxy.ts's own precise per-request\n\t// recording is the reliable \"network\" signal on this platform.\n\tif (/execvp\\(\\) of .* failed: Operation not permitted/i.test(stderr)) return \"filesystem\";\n\treturn \"unknown\";\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\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 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\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});\n\t\t\tconst proxyPort = proxy.port as number;\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\"(deny network*)\",\n\t\t\t\t'(allow network-outbound (remote ip (param \"PROXY_ADDR\")))',\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\tparams.push(\"-D\", `PROXY_ADDR=localhost:${proxyPort}`);\n\n\t\t\tconst child = spawn(\"sandbox-exec\", [\"-f\", profilePath, ...params, \"--\", launch.command, ...launch.args], {\n\t\t\t\tcwd: workspace,\n\t\t\t\tenv: {\n\t\t\t\t\t...launch.environment,\n\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\tHTTP_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t\tHTTPS_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t},\n\t\t\t\tstdio: [\"inherit\", \"inherit\", \"pipe\"],\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\tif (exitCode !== 0 && !proxyAlreadyRecordedAViolation) {\n\t\t\t\tconst kind = classifySandboxFailure(stderr);\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:\n\t\t\t\t\t\tkind === \"unknown\"\n\t\t\t\t\t\t\t? \"Sandboxed process exited unsuccessfully; inspect its stderr for the OS refusal.\"\n\t\t\t\t\t\t\t: 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\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,195 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createSandboxNetworkProxy } from "./network-proxy.js";
6
+ function sandboxExecExists(command) {
7
+ // sandbox-exec has no --version flag; a real, minimal profile run is the
8
+ // standard existence-and-basic-functionality probe (see the 2b.5 prototype's
9
+ // own positive control).
10
+ const result = spawnSync(command, ["-p", "(version 1)(allow default)", "/usr/bin/true"], {
11
+ stdio: "ignore",
12
+ timeout: 2_000,
13
+ });
14
+ return result.status === 0;
15
+ }
16
+ function waitForExit(child) {
17
+ return new Promise((resolve, reject) => {
18
+ child.once("error", reject);
19
+ child.once("exit", (code) => resolve(code ?? 1));
20
+ });
21
+ }
22
+ /**
23
+ * Unlike Linux's bwrap (which needs each mount ancestor declared for its bind-mount
24
+ * tree), Seatbelt's `subpath` is recursive from a single root, so only the directory
25
+ * containing each path is needed -- no ancestor walk. Each directory is resolved to
26
+ * its real (symlink-free) path: Seatbelt canonicalizes the path it actually checks
27
+ * against a `subpath` rule (confirmed empirically -- a rule built from an
28
+ * unresolved symlinked path, e.g. Homebrew's own layout, silently never matches).
29
+ */
30
+ function readOnlyDirectories(paths) {
31
+ const directories = paths.map((path) => dirname(resolve(path)));
32
+ const canonical = directories.map((directory) => {
33
+ try {
34
+ return realpathSync(directory);
35
+ }
36
+ catch {
37
+ return directory;
38
+ }
39
+ });
40
+ return [...new Set(canonical)];
41
+ }
42
+ function classifySandboxFailure(stderr) {
43
+ // Confirmed empirically (2b.5 prototype): unlike Linux's bwrap, macOS Seatbelt
44
+ // denials surface through the launched command's own generic "Operation not
45
+ // permitted"/EPERM text for filesystem AND network refusals alike -- there is no
46
+ // macOS-specific phrase equivalent to Linux's distinct "Network is unreachable".
47
+ // Only sandbox-exec's own wrapper-level exec refusal (a sign our own profile
48
+ // forgot to allow a binary, not a policy decision) is unambiguous from stderr
49
+ // text. Anything else stays "unknown"; network-proxy.ts's own precise per-request
50
+ // recording is the reliable "network" signal on this platform.
51
+ if (/execvp\(\) of .* failed: Operation not permitted/i.test(stderr))
52
+ return "filesystem";
53
+ return "unknown";
54
+ }
55
+ /**
56
+ * macOS's platform adapter. Seatbelt (`sandbox-exec`) allows reads broadly except
57
+ * the invoking account's home directory, denies writes outside the workspace, and
58
+ * denies network outside the sandbox proxy's exact loopback port -- narrower than
59
+ * the wildcard `localhost:*` pattern some other sandboxes use. This mirrors Linux's
60
+ * actual posture (`--ro-bind / /` plus a hidden `/home`) rather than the narrower
61
+ * read-allowlist this design started with, which broke ordinary process startup.
62
+ */
63
+ export function createMacosSandboxBackend(options) {
64
+ const platform = options?.platform ?? process.platform;
65
+ if (platform !== "darwin") {
66
+ return {
67
+ status: { kind: "unavailable", reason: "OS sandbox is supported on macOS only." },
68
+ async launch() {
69
+ throw new Error("macOS sandbox backend is unavailable.");
70
+ },
71
+ async close() { },
72
+ };
73
+ }
74
+ const hasCommand = options?.commandExists ?? sandboxExecExists;
75
+ const violationStore = options?.violationStore;
76
+ let proxy;
77
+ if (!hasCommand("sandbox-exec")) {
78
+ return {
79
+ status: { kind: "unavailable", reason: "sandbox-exec (Seatbelt) is required for OS sandboxing." },
80
+ async launch() {
81
+ throw new Error("macOS sandbox backend is unavailable.");
82
+ },
83
+ async close() { },
84
+ };
85
+ }
86
+ return {
87
+ status: { kind: "enforced" },
88
+ async launch(launch) {
89
+ // Same reasoning as readOnlyDirectories: on macOS the workspace itself can
90
+ // be reached through a symlink (e.g. os.tmpdir()'s /var -> /private/var),
91
+ // and Seatbelt's subpath match is against the canonical path, not the one
92
+ // the caller happened to spell. Resolve once, use everywhere below.
93
+ const workspace = realpathSync(launch.policy.workspace);
94
+ const stateDirectory = join(workspace, ".apex-code", "sandbox-state");
95
+ mkdirSync(stateDirectory, { recursive: true });
96
+ const violationCountBeforeLaunch = violationStore?.totalCount ?? 0;
97
+ proxy = await createSandboxNetworkProxy({
98
+ tcpHost: "127.0.0.1",
99
+ allowedHosts: launch.policy.allowedHosts,
100
+ violationStore,
101
+ });
102
+ const proxyPort = proxy.port;
103
+ const readOnlyDirs = readOnlyDirectories([process.execPath, launch.command, ...(launch.readOnlyPaths ?? [])]);
104
+ const readOnlyFiles = (launch.readOnlyFiles ?? []).map((path) => {
105
+ try {
106
+ return realpathSync(path);
107
+ }
108
+ catch {
109
+ return resolve(path);
110
+ }
111
+ });
112
+ let userHome;
113
+ try {
114
+ userHome = realpathSync(homedir());
115
+ }
116
+ catch {
117
+ userHome = homedir();
118
+ }
119
+ const profileLines = [
120
+ "(version 1)",
121
+ // bsd.sb's baseline is broader than Linux's opt-in bind-mount model --
122
+ // a real gap this design accepts, documented in the spec's fourth
123
+ // amendment -- but it is the only combination the 2b.5 prototype found
124
+ // that lets a normal Unix child process (dyld, /bin/sh) actually start.
125
+ '(import "bsd.sb")',
126
+ "(allow process-exec*)",
127
+ // Separate from process-exec* -- confirmed empirically: a shell that
128
+ // forks a child shell (e.g. `sh -c 'a' && sh -c 'b'`) failed with
129
+ // "fork: Operation not permitted" without this, even with exec allowed.
130
+ "(allow process-fork)",
131
+ // Matches Linux's actual posture (bwrap's `--ro-bind / /`): broad read,
132
+ // narrow write. A read-only allowlist alone (this design's first cut)
133
+ // breaks ordinary process startup -- confirmed empirically, getcwd()
134
+ // itself needs to traverse the workspace's ancestor directories, which
135
+ // no read rule scoped only to the workspace or runtime paths covers.
136
+ "(allow file-read*)",
137
+ '(deny file-read* (subpath (param "USER_HOME")))',
138
+ ...readOnlyDirs.map((_, index) => `(allow file-read* (subpath (param "RO_${index}")))`),
139
+ ...readOnlyFiles.map((_, index) => `(allow file-read* (subpath (param "RO_FILE_${index}")))`),
140
+ '(allow file-read* (subpath (param "WORKSPACE")))',
141
+ "(deny file-write*)",
142
+ '(allow file-write* (subpath (param "WORKSPACE")))',
143
+ "(deny network*)",
144
+ '(allow network-outbound (remote ip (param "PROXY_ADDR")))',
145
+ ];
146
+ const profilePath = join(stateDirectory, "profile.sb");
147
+ writeFileSync(profilePath, profileLines.join("\n"));
148
+ const params = ["-D", `USER_HOME=${userHome}`];
149
+ readOnlyDirs.forEach((dir, index) => {
150
+ params.push("-D", `RO_${index}=${dir}`);
151
+ });
152
+ readOnlyFiles.forEach((file, index) => {
153
+ params.push("-D", `RO_FILE_${index}=${file}`);
154
+ });
155
+ params.push("-D", `WORKSPACE=${workspace}`);
156
+ params.push("-D", `PROXY_ADDR=localhost:${proxyPort}`);
157
+ const child = spawn("sandbox-exec", ["-f", profilePath, ...params, "--", launch.command, ...launch.args], {
158
+ cwd: workspace,
159
+ env: {
160
+ ...launch.environment,
161
+ HOME: launch.environment?.HOME ?? stateDirectory,
162
+ TMPDIR: launch.environment?.TMPDIR ?? stateDirectory,
163
+ HTTP_PROXY: `http://127.0.0.1:${proxyPort}`,
164
+ HTTPS_PROXY: `http://127.0.0.1:${proxyPort}`,
165
+ },
166
+ stdio: ["inherit", "inherit", "pipe"],
167
+ });
168
+ let stderr = "";
169
+ child.stderr?.setEncoding("utf8");
170
+ child.stderr?.on("data", (chunk) => {
171
+ stderr += chunk;
172
+ process.stderr.write(chunk);
173
+ });
174
+ const exitCode = await waitForExit(child);
175
+ await proxy?.close();
176
+ const proxyAlreadyRecordedAViolation = (violationStore?.totalCount ?? 0) > violationCountBeforeLaunch;
177
+ if (exitCode !== 0 && !proxyAlreadyRecordedAViolation) {
178
+ const kind = classifySandboxFailure(stderr);
179
+ violationStore?.add({
180
+ kind,
181
+ command: [launch.command, ...launch.args].join(" "),
182
+ detail: kind === "unknown"
183
+ ? "Sandboxed process exited unsuccessfully; inspect its stderr for the OS refusal."
184
+ : stderr.trim(),
185
+ timestamp: new Date(),
186
+ });
187
+ }
188
+ return exitCode;
189
+ },
190
+ async close() {
191
+ await proxy?.close();
192
+ },
193
+ };
194
+ }
195
+ //# sourceMappingURL=macos-backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"macos-backend.js","sourceRoot":"","sources":["../../../src/core/sandbox/macos-backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, spawnSync } from \"node:child_process\";\nimport { mkdirSync, realpathSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { createSandboxNetworkProxy, type SandboxNetworkProxy } from \"./network-proxy.ts\";\nimport type { SandboxBackend, SandboxLaunch } from \"./supervisor.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}\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\nfunction classifySandboxFailure(stderr: string): \"filesystem\" | \"network\" | \"unknown\" {\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 stays \"unknown\"; network-proxy.ts's own precise per-request\n\t// recording is the reliable \"network\" signal on this platform.\n\tif (/execvp\\(\\) of .* failed: Operation not permitted/i.test(stderr)) return \"filesystem\";\n\treturn \"unknown\";\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\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 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\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});\n\t\t\tconst proxyPort = proxy.port as number;\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\"(deny network*)\",\n\t\t\t\t'(allow network-outbound (remote ip (param \"PROXY_ADDR\")))',\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\tparams.push(\"-D\", `PROXY_ADDR=localhost:${proxyPort}`);\n\n\t\t\tconst child = spawn(\"sandbox-exec\", [\"-f\", profilePath, ...params, \"--\", launch.command, ...launch.args], {\n\t\t\t\tcwd: workspace,\n\t\t\t\tenv: {\n\t\t\t\t\t...launch.environment,\n\t\t\t\t\tHOME: launch.environment?.HOME ?? stateDirectory,\n\t\t\t\t\tTMPDIR: launch.environment?.TMPDIR ?? stateDirectory,\n\t\t\t\t\tHTTP_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t\tHTTPS_PROXY: `http://127.0.0.1:${proxyPort}`,\n\t\t\t\t},\n\t\t\t\tstdio: [\"inherit\", \"inherit\", \"pipe\"],\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\tif (exitCode !== 0 && !proxyAlreadyRecordedAViolation) {\n\t\t\t\tconst kind = classifySandboxFailure(stderr);\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:\n\t\t\t\t\t\tkind === \"unknown\"\n\t\t\t\t\t\t\t? \"Sandboxed process exited unsuccessfully; inspect its stderr for the OS refusal.\"\n\t\t\t\t\t\t\t: 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\tawait proxy?.close();\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import * as http from "node:http";
2
+ import type { SandboxViolationStore } from "./violations.ts";
3
+ export interface SandboxNetworkProxy {
4
+ readonly server: http.Server;
5
+ /** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */
6
+ readonly port?: number;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare function createSandboxNetworkProxy(options: {
10
+ allowedHosts: readonly string[];
11
+ violationStore?: SandboxViolationStore;
12
+ } & ({
13
+ socketPath: string;
14
+ tcpHost?: undefined;
15
+ } | {
16
+ socketPath?: undefined;
17
+ tcpHost: string;
18
+ })): Promise<SandboxNetworkProxy>;
19
+ //# sourceMappingURL=network-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-proxy.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/network-proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE;IACR,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACvC,GAAG,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,UAAU,CAAC,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7F,OAAO,CAAC,mBAAmB,CAAC,CAqF9B","sourcesContent":["import { existsSync, unlinkSync } from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface SandboxNetworkProxy {\n\treadonly server: http.Server;\n\t/** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */\n\treadonly port?: number;\n\tclose(): Promise<void>;\n}\n\nexport function createSandboxNetworkProxy(\n\toptions: {\n\t\tallowedHosts: readonly string[];\n\t\tviolationStore?: SandboxViolationStore;\n\t} & ({ socketPath: string; tcpHost?: undefined } | { socketPath?: undefined; tcpHost: string }),\n): Promise<SandboxNetworkProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = http.createServer((_req, res) => {\n\t\t\tres.writeHead(405, { \"Content-Type\": \"text/plain\" });\n\t\t\tres.end(\"Method Not Allowed\\n\");\n\t\t});\n\n\t\tserver.on(\"connect\", (req, clientSocket, head) => {\n\t\t\tconst match = req.url?.match(/^([^:]+):(\\d+)$/);\n\t\t\tif (!match) {\n\t\t\t\tclientSocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst hostname = match[1];\n\t\t\tconst port = Number.parseInt(match[2], 10);\n\n\t\t\tconst isAllowed =\n\t\t\t\toptions.allowedHosts.includes(hostname) || options.allowedHosts.includes(`${hostname}:${port}`);\n\t\t\tif (!isAllowed) {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n\t\t\t\tclientSocket.destroy();\n\t\t\t\toptions.violationStore?.add({\n\t\t\t\t\tkind: \"network\",\n\t\t\t\t\tcommand: `CONNECT ${hostname}:${port}`,\n\t\t\t\t\tdetail: `Host ${hostname} refused by allowlist policy.`,\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst serverSocket = net.connect(Number(port) || 443, hostname, () => {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n\t\t\t\tserverSocket.write(head);\n\t\t\t\tserverSocket.pipe(clientSocket);\n\t\t\t\tclientSocket.pipe(serverSocket);\n\t\t\t});\n\n\t\t\tserverSocket.on(\"error\", () => {\n\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\tclientSocket.write(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tclientSocket.on(\"error\", () => {\n\t\t\t\tserverSocket.destroy();\n\t\t\t});\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\n\t\tif (options.tcpHost !== undefined) {\n\t\t\t// macOS's Seatbelt can permit a specific localhost port directly, so the\n\t\t\t// proxy listens on loopback TCP there instead of a Unix domain socket --\n\t\t\t// no in-child relay process is needed the way Linux's `--unshare-net`\n\t\t\t// requires one.\n\t\t\tserver.listen(0, options.tcpHost, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tconst address = server.address();\n\t\t\t\tconst port = typeof address === \"object\" && address !== null ? address.port : undefined;\n\t\t\t\tresolve({\n\t\t\t\t\tserver,\n\t\t\t\t\tport,\n\t\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// A prior sandboxed launch that never called close() (a crash, a SIGKILL)\n\t\t// leaves a stale socket file behind; Node's listen() refuses to bind over\n\t\t// one even though nothing is listening on it.\n\t\tif (existsSync(options.socketPath)) {\n\t\t\tunlinkSync(options.socketPath);\n\t\t}\n\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import { existsSync, unlinkSync } from "node:fs";
2
+ import * as http from "node:http";
3
+ import * as net from "node:net";
4
+ export function createSandboxNetworkProxy(options) {
5
+ return new Promise((resolve, reject) => {
6
+ const server = http.createServer((_req, res) => {
7
+ res.writeHead(405, { "Content-Type": "text/plain" });
8
+ res.end("Method Not Allowed\n");
9
+ });
10
+ server.on("connect", (req, clientSocket, head) => {
11
+ const match = req.url?.match(/^([^:]+):(\d+)$/);
12
+ if (!match) {
13
+ clientSocket.destroy();
14
+ return;
15
+ }
16
+ const hostname = match[1];
17
+ const port = Number.parseInt(match[2], 10);
18
+ const isAllowed = options.allowedHosts.includes(hostname) || options.allowedHosts.includes(`${hostname}:${port}`);
19
+ if (!isAllowed) {
20
+ clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
21
+ clientSocket.destroy();
22
+ options.violationStore?.add({
23
+ kind: "network",
24
+ command: `CONNECT ${hostname}:${port}`,
25
+ detail: `Host ${hostname} refused by allowlist policy.`,
26
+ timestamp: new Date(),
27
+ });
28
+ return;
29
+ }
30
+ const serverSocket = net.connect(Number(port) || 443, hostname, () => {
31
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
32
+ serverSocket.write(head);
33
+ serverSocket.pipe(clientSocket);
34
+ clientSocket.pipe(serverSocket);
35
+ });
36
+ serverSocket.on("error", () => {
37
+ if (!clientSocket.destroyed) {
38
+ clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
39
+ clientSocket.destroy();
40
+ }
41
+ });
42
+ clientSocket.on("error", () => {
43
+ serverSocket.destroy();
44
+ });
45
+ });
46
+ server.on("error", reject);
47
+ if (options.tcpHost !== undefined) {
48
+ // macOS's Seatbelt can permit a specific localhost port directly, so the
49
+ // proxy listens on loopback TCP there instead of a Unix domain socket --
50
+ // no in-child relay process is needed the way Linux's `--unshare-net`
51
+ // requires one.
52
+ server.listen(0, options.tcpHost, () => {
53
+ server.off("error", reject);
54
+ const address = server.address();
55
+ const port = typeof address === "object" && address !== null ? address.port : undefined;
56
+ resolve({
57
+ server,
58
+ port,
59
+ close: () => new Promise((r) => server.close(() => r())),
60
+ });
61
+ });
62
+ return;
63
+ }
64
+ // A prior sandboxed launch that never called close() (a crash, a SIGKILL)
65
+ // leaves a stale socket file behind; Node's listen() refuses to bind over
66
+ // one even though nothing is listening on it.
67
+ if (existsSync(options.socketPath)) {
68
+ unlinkSync(options.socketPath);
69
+ }
70
+ server.listen(options.socketPath, () => {
71
+ server.off("error", reject);
72
+ resolve({
73
+ server,
74
+ close: () => new Promise((r) => server.close(() => r())),
75
+ });
76
+ });
77
+ });
78
+ }
79
+ //# sourceMappingURL=network-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-proxy.js","sourceRoot":"","sources":["../../../src/core/sandbox/network-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, unlinkSync } from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as net from \"node:net\";\nimport type { SandboxViolationStore } from \"./violations.ts\";\n\nexport interface SandboxNetworkProxy {\n\treadonly server: http.Server;\n\t/** Set only in TCP-listen mode (`tcpHost`); the ephemeral port the proxy bound. */\n\treadonly port?: number;\n\tclose(): Promise<void>;\n}\n\nexport function createSandboxNetworkProxy(\n\toptions: {\n\t\tallowedHosts: readonly string[];\n\t\tviolationStore?: SandboxViolationStore;\n\t} & ({ socketPath: string; tcpHost?: undefined } | { socketPath?: undefined; tcpHost: string }),\n): Promise<SandboxNetworkProxy> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = http.createServer((_req, res) => {\n\t\t\tres.writeHead(405, { \"Content-Type\": \"text/plain\" });\n\t\t\tres.end(\"Method Not Allowed\\n\");\n\t\t});\n\n\t\tserver.on(\"connect\", (req, clientSocket, head) => {\n\t\t\tconst match = req.url?.match(/^([^:]+):(\\d+)$/);\n\t\t\tif (!match) {\n\t\t\t\tclientSocket.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst hostname = match[1];\n\t\t\tconst port = Number.parseInt(match[2], 10);\n\n\t\t\tconst isAllowed =\n\t\t\t\toptions.allowedHosts.includes(hostname) || options.allowedHosts.includes(`${hostname}:${port}`);\n\t\t\tif (!isAllowed) {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n\t\t\t\tclientSocket.destroy();\n\t\t\t\toptions.violationStore?.add({\n\t\t\t\t\tkind: \"network\",\n\t\t\t\t\tcommand: `CONNECT ${hostname}:${port}`,\n\t\t\t\t\tdetail: `Host ${hostname} refused by allowlist policy.`,\n\t\t\t\t\ttimestamp: new Date(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst serverSocket = net.connect(Number(port) || 443, hostname, () => {\n\t\t\t\tclientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n\t\t\t\tserverSocket.write(head);\n\t\t\t\tserverSocket.pipe(clientSocket);\n\t\t\t\tclientSocket.pipe(serverSocket);\n\t\t\t});\n\n\t\t\tserverSocket.on(\"error\", () => {\n\t\t\t\tif (!clientSocket.destroyed) {\n\t\t\t\t\tclientSocket.write(`HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n`);\n\t\t\t\t\tclientSocket.destroy();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tclientSocket.on(\"error\", () => {\n\t\t\t\tserverSocket.destroy();\n\t\t\t});\n\t\t});\n\n\t\tserver.on(\"error\", reject);\n\n\t\tif (options.tcpHost !== undefined) {\n\t\t\t// macOS's Seatbelt can permit a specific localhost port directly, so the\n\t\t\t// proxy listens on loopback TCP there instead of a Unix domain socket --\n\t\t\t// no in-child relay process is needed the way Linux's `--unshare-net`\n\t\t\t// requires one.\n\t\t\tserver.listen(0, options.tcpHost, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tconst address = server.address();\n\t\t\t\tconst port = typeof address === \"object\" && address !== null ? address.port : undefined;\n\t\t\t\tresolve({\n\t\t\t\t\tserver,\n\t\t\t\t\tport,\n\t\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// A prior sandboxed launch that never called close() (a crash, a SIGKILL)\n\t\t// leaves a stale socket file behind; Node's listen() refuses to bind over\n\t\t// one even though nothing is listening on it.\n\t\tif (existsSync(options.socketPath)) {\n\t\t\tunlinkSync(options.socketPath);\n\t\t}\n\n\t\tserver.listen(options.socketPath, () => {\n\t\t\tserver.off(\"error\", reject);\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tclose: () => new Promise((r) => server.close(() => r())),\n\t\t\t});\n\t\t});\n\t});\n}\n"]}