windows-development-sandbox 1.0.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/config.example.json +55 -5
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +23 -0
  6. package/dist/cli/args.js +38 -4
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/accounts.js +2 -2
  10. package/dist/cli/commands/agent-commands.d.ts +6 -0
  11. package/dist/cli/commands/agent-commands.js +61 -0
  12. package/dist/cli/commands/context.d.ts +2 -0
  13. package/dist/cli/commands/context.js +5 -1
  14. package/dist/cli/commands/data/clean.d.ts +15 -0
  15. package/dist/cli/commands/data/clean.js +106 -0
  16. package/dist/cli/commands/data/collect.d.ts +11 -0
  17. package/dist/cli/commands/data/collect.js +54 -0
  18. package/dist/cli/commands/data/index.d.ts +4 -0
  19. package/dist/cli/commands/data/index.js +45 -0
  20. package/dist/cli/commands/data/remove.d.ts +2 -0
  21. package/dist/cli/commands/data/remove.js +30 -0
  22. package/dist/cli/commands/index.d.ts +6 -4
  23. package/dist/cli/commands/index.js +6 -4
  24. package/dist/cli/commands/launch.d.ts +26 -2
  25. package/dist/cli/commands/launch.js +256 -42
  26. package/dist/cli/commands/maintenance.d.ts +2 -0
  27. package/dist/cli/commands/maintenance.js +67 -5
  28. package/dist/cli/commands/project.d.ts +29 -0
  29. package/dist/cli/commands/project.js +63 -0
  30. package/dist/cli/commands/projects.d.ts +14 -0
  31. package/dist/cli/commands/projects.js +146 -0
  32. package/dist/cli/commands/repair/checks.d.ts +2 -0
  33. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  34. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  35. package/dist/cli/commands/repair/finders.js +108 -0
  36. package/dist/cli/commands/repair/index.d.ts +12 -0
  37. package/dist/cli/commands/repair/index.js +61 -0
  38. package/dist/cli/commands/repair/types.d.ts +26 -0
  39. package/dist/cli/commands/repair/types.js +3 -0
  40. package/dist/cli/commands/restart.d.ts +6 -0
  41. package/dist/cli/commands/restart.js +85 -0
  42. package/dist/cli/commands/sessions.d.ts +2 -1
  43. package/dist/cli/commands/sessions.js +189 -45
  44. package/dist/cli/commands/setup/checks.d.ts +13 -0
  45. package/dist/cli/commands/setup/checks.js +71 -0
  46. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  47. package/dist/cli/commands/setup/facts.js +60 -0
  48. package/dist/cli/commands/setup/index.d.ts +2 -0
  49. package/dist/cli/commands/setup/index.js +43 -0
  50. package/dist/cli/commands/user-config.d.ts +8 -0
  51. package/dist/cli/commands/user-config.js +41 -0
  52. package/dist/cli/history.d.ts +11 -0
  53. package/dist/cli/history.js +21 -0
  54. package/dist/cli/index.d.ts +7 -1
  55. package/dist/cli/index.js +42 -15
  56. package/dist/cli/manifest.d.ts +8 -0
  57. package/dist/cli/manifest.js +40 -15
  58. package/dist/cli/menu/apps.d.ts +21 -0
  59. package/dist/cli/menu/apps.js +122 -0
  60. package/dist/cli/menu/command-form.d.ts +28 -0
  61. package/dist/cli/menu/command-form.js +219 -0
  62. package/dist/cli/menu/index.d.ts +26 -0
  63. package/dist/cli/menu/index.js +180 -0
  64. package/dist/cli/menu/settings/categories.d.ts +9 -0
  65. package/dist/cli/menu/settings/categories.js +28 -0
  66. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  67. package/dist/cli/menu/settings/config-file.js +71 -0
  68. package/dist/cli/menu/settings/editors.d.ts +30 -0
  69. package/dist/cli/menu/settings/editors.js +305 -0
  70. package/dist/cli/menu/settings/index.d.ts +32 -0
  71. package/dist/cli/menu/settings/index.js +136 -0
  72. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  73. package/dist/cli/menu/settings/schema-node.js +104 -0
  74. package/dist/cli/menu/several.d.ts +20 -0
  75. package/dist/cli/menu/several.js +102 -0
  76. package/dist/cli/progress.d.ts +9 -0
  77. package/dist/cli/progress.js +49 -0
  78. package/dist/cli/tui/app.d.ts +39 -0
  79. package/dist/cli/tui/app.js +52 -0
  80. package/dist/cli/tui/dialogs.d.ts +57 -0
  81. package/dist/cli/tui/dialogs.js +162 -0
  82. package/dist/cli/tui/frame.d.ts +18 -0
  83. package/dist/cli/tui/frame.js +27 -0
  84. package/dist/cli/tui/index.d.ts +7 -0
  85. package/dist/cli/tui/index.js +8 -0
  86. package/dist/cli/tui/list-screen.d.ts +101 -0
  87. package/dist/cli/tui/list-screen.js +299 -0
  88. package/dist/cli/tui/terminal.d.ts +37 -0
  89. package/dist/cli/tui/terminal.js +89 -0
  90. package/dist/cli/tui/text.d.ts +20 -0
  91. package/dist/cli/tui/text.js +105 -0
  92. package/dist/cli/tui/theme.d.ts +45 -0
  93. package/dist/cli/tui/theme.js +30 -0
  94. package/dist/cli/validate.d.ts +17 -0
  95. package/dist/cli/validate.js +89 -0
  96. package/dist/core/config.js +12 -6
  97. package/dist/core/disk.d.ts +16 -0
  98. package/dist/core/disk.js +55 -0
  99. package/dist/core/local-config.d.ts +23 -0
  100. package/dist/core/local-config.js +79 -7
  101. package/dist/core/project-policy.d.ts +15 -0
  102. package/dist/core/project-policy.js +35 -0
  103. package/dist/core/prompt.d.ts +4 -0
  104. package/dist/core/prompt.js +15 -0
  105. package/dist/core/safe-paths.d.ts +4 -0
  106. package/dist/core/safe-paths.js +37 -0
  107. package/dist/core/schema/apps.d.ts +40 -0
  108. package/dist/core/schema/apps.js +27 -0
  109. package/dist/core/schema/common.d.ts +17 -0
  110. package/dist/core/schema/common.js +15 -0
  111. package/dist/core/schema/config.d.ts +659 -0
  112. package/dist/core/schema/config.js +75 -0
  113. package/dist/core/schema/databases.d.ts +33 -0
  114. package/dist/core/schema/databases.js +35 -0
  115. package/dist/core/schema/harness.d.ts +12 -0
  116. package/dist/core/schema/harness.js +17 -0
  117. package/dist/core/schema/json-schema.d.ts +5 -0
  118. package/dist/core/schema/json-schema.js +25 -0
  119. package/dist/core/schema/profile.d.ts +32 -0
  120. package/dist/core/schema/profile.js +31 -0
  121. package/dist/core/schema/projects.d.ts +9 -0
  122. package/dist/core/schema/projects.js +11 -0
  123. package/dist/core/schema/sandbox.d.ts +78 -0
  124. package/dist/core/schema/sandbox.js +64 -0
  125. package/dist/core/schema/toolchains.d.ts +96 -0
  126. package/dist/core/schema/toolchains.js +88 -0
  127. package/dist/core/schema.d.ts +6 -604
  128. package/dist/core/schema.js +8 -264
  129. package/dist/core/types.d.ts +35 -1
  130. package/dist/core/util.d.ts +9 -0
  131. package/dist/core/util.js +14 -3
  132. package/dist/core/workspace.d.ts +0 -5
  133. package/dist/core/workspace.js +33 -50
  134. package/dist/guest/apps/index.d.ts +10 -0
  135. package/dist/guest/apps/index.js +206 -0
  136. package/dist/guest/apps/resolvers.d.ts +8 -0
  137. package/dist/guest/apps/resolvers.js +90 -0
  138. package/dist/guest/audit-parse.d.ts +7 -0
  139. package/dist/guest/audit-parse.js +24 -0
  140. package/dist/guest/audit.d.ts +4 -0
  141. package/dist/guest/audit.js +47 -0
  142. package/dist/guest/boot-env.d.ts +4 -0
  143. package/dist/guest/boot-env.js +25 -0
  144. package/dist/guest/cmdline.d.ts +12 -0
  145. package/dist/guest/cmdline.js +23 -0
  146. package/dist/guest/daemon/exec.d.ts +5 -0
  147. package/dist/guest/daemon/exec.js +32 -0
  148. package/dist/guest/daemon/project.d.ts +14 -0
  149. package/dist/guest/daemon/project.js +132 -0
  150. package/dist/guest/daemon/relays.d.ts +1 -0
  151. package/dist/guest/daemon/relays.js +18 -0
  152. package/dist/guest/daemon/session.d.ts +13 -0
  153. package/dist/guest/daemon/session.js +139 -0
  154. package/dist/guest/daemon/shutdown.d.ts +1 -0
  155. package/dist/guest/daemon/shutdown.js +21 -0
  156. package/dist/guest/daemon/state.d.ts +17 -0
  157. package/dist/guest/daemon/state.js +31 -0
  158. package/dist/guest/daemon.js +12 -141
  159. package/dist/guest/db.js +18 -3
  160. package/dist/guest/env.d.ts +3 -0
  161. package/dist/guest/env.js +7 -3
  162. package/dist/guest/guide.d.ts +6 -2
  163. package/dist/guest/guide.js +5 -4
  164. package/dist/guest/helpers.d.ts +3 -0
  165. package/dist/guest/helpers.js +26 -0
  166. package/dist/guest/main.js +31 -8
  167. package/dist/guest/netproxy.d.ts +1 -0
  168. package/dist/guest/netproxy.js +100 -0
  169. package/dist/guest/ports.d.ts +6 -0
  170. package/dist/guest/ports.js +22 -0
  171. package/dist/guest/project-context.d.ts +7 -0
  172. package/dist/guest/project-context.js +10 -0
  173. package/dist/guest/project-env.d.ts +20 -0
  174. package/dist/guest/project-env.js +20 -0
  175. package/dist/guest/project-status.d.ts +5 -0
  176. package/dist/guest/project-status.js +17 -0
  177. package/dist/guest/ptyhost.d.ts +0 -18
  178. package/dist/guest/ptyhost.js +17 -65
  179. package/dist/guest/setup/agents.d.ts +5 -2
  180. package/dist/guest/setup/agents.js +22 -8
  181. package/dist/guest/setup/claude.d.ts +0 -4
  182. package/dist/guest/setup/claude.js +59 -5
  183. package/dist/guest/setup/environment.d.ts +13 -3
  184. package/dist/guest/setup/environment.js +14 -10
  185. package/dist/guest/setup/hooks.d.ts +9 -1
  186. package/dist/guest/setup/hooks.js +28 -14
  187. package/dist/guest/setup/network.d.ts +9 -0
  188. package/dist/guest/setup/network.js +97 -4
  189. package/dist/guest/setup/user.js +4 -0
  190. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  191. package/dist/guest/toolchains/catalogue.js +2 -2
  192. package/dist/guest/toolchains/cli.js +25 -17
  193. package/dist/guest/toolchains/common.d.ts +18 -1
  194. package/dist/guest/toolchains/common.js +34 -11
  195. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  196. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  197. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  198. package/dist/guest/toolchains/databases/index.js +62 -0
  199. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  200. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  201. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mysql.js +38 -0
  203. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  204. package/dist/guest/toolchains/databases/postgres.js +38 -0
  205. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  206. package/dist/guest/toolchains/databases/redis.js +16 -0
  207. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  208. package/dist/guest/toolchains/databases/server.js +52 -0
  209. package/dist/guest/toolchains/git.js +25 -16
  210. package/dist/guest/toolchains/go.js +1 -1
  211. package/dist/guest/toolchains/index.d.ts +13 -9
  212. package/dist/guest/toolchains/index.js +40 -82
  213. package/dist/guest/toolchains/java.js +5 -5
  214. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  215. package/dist/guest/toolchains/leftovers.js +26 -0
  216. package/dist/guest/toolchains/managers.js +12 -10
  217. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  218. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  219. package/dist/guest/toolchains/native/cpp.js +38 -0
  220. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  221. package/dist/guest/toolchains/native/dotnet.js +103 -0
  222. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  223. package/dist/guest/toolchains/native/mesa.js +56 -0
  224. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  225. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  226. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  227. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  228. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  229. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  230. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  231. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  232. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  233. package/dist/guest/toolchains/native/rust.js +45 -0
  234. package/dist/guest/toolchains/node.js +26 -6
  235. package/dist/guest/toolchains/plan.d.ts +24 -0
  236. package/dist/guest/toolchains/plan.js +140 -0
  237. package/dist/guest/toolchains/pwsh.js +1 -1
  238. package/dist/guest/toolchains/python.js +43 -12
  239. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  240. package/dist/guest/toolchains/runtimes.js +6 -8
  241. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  242. package/dist/guest/toolchains/scripting/php.js +71 -0
  243. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  244. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  245. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  246. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  247. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  248. package/dist/guest/toolchains/vscode-profile.js +69 -0
  249. package/dist/guest/toolchains/vscode.d.ts +17 -0
  250. package/dist/guest/toolchains/vscode.js +87 -1
  251. package/dist/guest/view.js +21 -1
  252. package/dist/host/accounts.js +14 -5
  253. package/dist/host/agent-commands.d.ts +25 -0
  254. package/dist/host/agent-commands.js +145 -0
  255. package/dist/host/attach.d.ts +17 -0
  256. package/dist/host/attach.js +70 -0
  257. package/dist/host/audit/collect.d.ts +34 -0
  258. package/dist/host/audit/collect.js +126 -0
  259. package/dist/host/audit/log.d.ts +46 -0
  260. package/dist/host/audit/log.js +143 -0
  261. package/dist/host/channel.d.ts +23 -22
  262. package/dist/host/channel.js +66 -84
  263. package/dist/host/claude-profile.js +7 -3
  264. package/dist/host/cleanup.js +2 -11
  265. package/dist/host/database-plan.d.ts +12 -0
  266. package/dist/host/database-plan.js +52 -0
  267. package/dist/host/generate.d.ts +10 -12
  268. package/dist/host/generate.js +58 -135
  269. package/dist/host/join.d.ts +28 -0
  270. package/dist/host/join.js +76 -0
  271. package/dist/host/launch/mappings.d.ts +3 -0
  272. package/dist/host/launch/mappings.js +54 -0
  273. package/dist/host/launch/projects.d.ts +15 -0
  274. package/dist/host/launch/projects.js +56 -0
  275. package/dist/host/launch/stage.d.ts +11 -0
  276. package/dist/host/launch/stage.js +66 -0
  277. package/dist/host/launch/wsb.d.ts +10 -0
  278. package/dist/host/launch/wsb.js +34 -0
  279. package/dist/host/motw.d.ts +17 -0
  280. package/dist/host/motw.js +74 -0
  281. package/dist/host/pc-scan.d.ts +6 -0
  282. package/dist/host/pc-scan.js +37 -0
  283. package/dist/host/platform.d.ts +2 -0
  284. package/dist/host/platform.js +16 -1
  285. package/dist/host/port-forward.d.ts +8 -0
  286. package/dist/host/port-forward.js +37 -0
  287. package/dist/host/project-scan.d.ts +1 -17
  288. package/dist/host/project-scan.js +2 -86
  289. package/dist/host/projects.d.ts +100 -0
  290. package/dist/host/projects.js +147 -0
  291. package/dist/host/repo-guard.d.ts +25 -5
  292. package/dist/host/repo-guard.js +242 -23
  293. package/dist/host/stops.d.ts +67 -0
  294. package/dist/host/stops.js +126 -0
  295. package/dist/host/terminal.d.ts +8 -0
  296. package/dist/host/terminal.js +48 -1
  297. package/dist/host/test-guard.d.ts +6 -0
  298. package/dist/host/test-guard.js +15 -0
  299. package/dist/host/window.js +7 -3
  300. package/dist/host/workspace-view/index.d.ts +20 -0
  301. package/dist/host/workspace-view/index.js +53 -0
  302. package/dist/host/workspace-view/rules.d.ts +26 -0
  303. package/dist/host/workspace-view/rules.js +93 -0
  304. package/dist/host/workspace-view/view.d.ts +50 -0
  305. package/dist/host/workspace-view/view.js +281 -0
  306. package/dist/host/wsb.d.ts +37 -0
  307. package/dist/host/wsb.js +101 -0
  308. package/dist/runtime/daemon.js +11791 -2821
  309. package/dist/runtime/db.js +6585 -466
  310. package/dist/runtime/main.js +10238 -1724
  311. package/dist/runtime/netproxy.js +25936 -0
  312. package/dist/runtime/view.js +6183 -59
  313. package/dist/selftest/accounts.test.d.ts +2 -0
  314. package/dist/selftest/accounts.test.js +44 -0
  315. package/dist/selftest/agent-commands.test.d.ts +2 -0
  316. package/dist/selftest/agent-commands.test.js +47 -0
  317. package/dist/selftest/audit.test.d.ts +2 -0
  318. package/dist/selftest/audit.test.js +57 -0
  319. package/dist/selftest/channel.test.d.ts +2 -0
  320. package/dist/selftest/channel.test.js +44 -0
  321. package/dist/selftest/cli.test.d.ts +2 -2
  322. package/dist/selftest/cli.test.js +145 -27
  323. package/dist/selftest/cmdline.test.d.ts +2 -0
  324. package/dist/selftest/cmdline.test.js +45 -0
  325. package/dist/selftest/config.test.d.ts +2 -3
  326. package/dist/selftest/config.test.js +36 -72
  327. package/dist/selftest/data.test.d.ts +2 -2
  328. package/dist/selftest/data.test.js +77 -70
  329. package/dist/selftest/detect.test.d.ts +2 -2
  330. package/dist/selftest/detect.test.js +54 -38
  331. package/dist/selftest/fixtures.d.ts +89 -4
  332. package/dist/selftest/fixtures.js +43 -24
  333. package/dist/selftest/generate.test.d.ts +2 -0
  334. package/dist/selftest/generate.test.js +68 -0
  335. package/dist/selftest/git-access.test.d.ts +2 -0
  336. package/dist/selftest/git-access.test.js +26 -0
  337. package/dist/selftest/guest-boot.test.d.ts +2 -0
  338. package/dist/selftest/guest-boot.test.js +76 -0
  339. package/dist/selftest/harness.d.ts +44 -7
  340. package/dist/selftest/harness.js +62 -21
  341. package/dist/selftest/host-sim.test.d.ts +2 -0
  342. package/dist/selftest/host-sim.test.js +93 -0
  343. package/dist/selftest/hostile-input.test.d.ts +2 -0
  344. package/dist/selftest/hostile-input.test.js +54 -0
  345. package/dist/selftest/index.d.ts +8 -1
  346. package/dist/selftest/index.js +137 -35
  347. package/dist/selftest/links.test.d.ts +2 -0
  348. package/dist/selftest/links.test.js +50 -0
  349. package/dist/selftest/live-daemon.test.d.ts +2 -0
  350. package/dist/selftest/live-daemon.test.js +91 -0
  351. package/dist/selftest/live-git.test.d.ts +2 -0
  352. package/dist/selftest/live-git.test.js +60 -0
  353. package/dist/selftest/live-network.test.d.ts +2 -0
  354. package/dist/selftest/live-network.test.js +63 -0
  355. package/dist/selftest/live-projects.test.d.ts +2 -0
  356. package/dist/selftest/live-projects.test.js +127 -0
  357. package/dist/selftest/live.d.ts +65 -0
  358. package/dist/selftest/live.js +109 -0
  359. package/dist/selftest/local-config.test.d.ts +2 -0
  360. package/dist/selftest/local-config.test.js +120 -0
  361. package/dist/selftest/mirror.test.d.ts +2 -3
  362. package/dist/selftest/mirror.test.js +92 -65
  363. package/dist/selftest/network.test.d.ts +2 -0
  364. package/dist/selftest/network.test.js +40 -0
  365. package/dist/selftest/personalization.test.d.ts +2 -0
  366. package/dist/selftest/personalization.test.js +53 -0
  367. package/dist/selftest/projects.test.d.ts +2 -0
  368. package/dist/selftest/projects.test.js +254 -0
  369. package/dist/selftest/repair.test.d.ts +2 -0
  370. package/dist/selftest/repair.test.js +42 -0
  371. package/dist/selftest/repo-guard.test.d.ts +2 -0
  372. package/dist/selftest/repo-guard.test.js +121 -0
  373. package/dist/selftest/secrets.test.d.ts +2 -0
  374. package/dist/selftest/secrets.test.js +71 -0
  375. package/dist/selftest/setup.test.d.ts +2 -2
  376. package/dist/selftest/setup.test.js +45 -22
  377. package/dist/selftest/suite.d.ts +13 -0
  378. package/dist/selftest/suite.js +1 -0
  379. package/dist/selftest/sync.test.d.ts +2 -3
  380. package/dist/selftest/sync.test.js +130 -78
  381. package/dist/selftest/test-guard.test.d.ts +2 -0
  382. package/dist/selftest/test-guard.test.js +49 -0
  383. package/dist/selftest/toolchains.test.d.ts +2 -0
  384. package/dist/selftest/toolchains.test.js +133 -0
  385. package/dist/selftest/workspace-view.test.d.ts +2 -0
  386. package/dist/selftest/workspace-view.test.js +89 -0
  387. package/dist/selftest/workspace.test.d.ts +2 -0
  388. package/dist/selftest/workspace.test.js +53 -0
  389. package/dist/shared/apps.d.ts +49 -0
  390. package/dist/shared/apps.js +114 -0
  391. package/dist/shared/download.d.ts +3 -1
  392. package/dist/shared/download.js +19 -4
  393. package/dist/shared/netpolicy.d.ts +27 -0
  394. package/dist/shared/netpolicy.js +51 -0
  395. package/dist/shared/paths.d.ts +5 -0
  396. package/dist/shared/paths.js +6 -0
  397. package/dist/shared/projects.d.ts +53 -0
  398. package/dist/shared/projects.js +67 -0
  399. package/dist/shared/pty-client.d.ts +9 -4
  400. package/dist/shared/pty-client.js +139 -17
  401. package/dist/shared/rundir.d.ts +12 -1
  402. package/dist/shared/rundir.js +14 -4
  403. package/dist/shared/safe-remove.js +11 -0
  404. package/dist/shared/schema.d.ts +453 -0
  405. package/dist/shared/schema.js +43 -2
  406. package/dist/shared/system.d.ts +1 -0
  407. package/dist/shared/system.js +1 -0
  408. package/dist/shared/test-guard.d.ts +4 -0
  409. package/dist/shared/test-guard.js +37 -0
  410. package/dist/sync/claude-mem.js +6 -3
  411. package/dist/sync/history.d.ts +29 -12
  412. package/dist/sync/history.js +72 -25
  413. package/dist/sync/watcher.d.ts +8 -0
  414. package/dist/sync/watcher.js +99 -8
  415. package/package.json +7 -6
  416. package/sandbox/bootstrap.ps1 +258 -258
  417. package/sandbox/cargoshim.cs +56 -0
  418. package/sandbox/cpucap.cs +33 -0
  419. package/sandbox/cpucap.ps1 +20 -0
  420. package/sandbox/logon.cmd +24 -16
  421. package/sandbox/memlog.ps1 +34 -0
  422. package/sandbox/openssl.cs +42 -0
  423. package/sandbox/pagefile.cs +31 -0
  424. package/sandbox/pagefile.ps1 +21 -0
  425. package/sandbox/ptyhost.cs +120 -11
  426. package/sandbox/trust.cmd +35 -35
  427. package/schemas/config.schema.json +403 -3
  428. package/schemas/sandbox-config.schema.json +143 -13
  429. package/dist/cli/commands/data.d.ts +0 -46
  430. package/dist/cli/commands/data.js +0 -249
  431. package/dist/cli/commands/setup.js +0 -152
  432. package/dist/cli/menu.d.ts +0 -22
  433. package/dist/cli/menu.js +0 -187
  434. package/dist/cli/settings.d.ts +0 -11
  435. package/dist/cli/settings.js +0 -156
  436. package/dist/guest/toolchains/databases.js +0 -226
  437. package/dist/guest/toolchains/msvc.d.ts +0 -10
  438. package/dist/guest/toolchains/msvc.js +0 -216
  439. package/dist/guest/toolchains/native.d.ts +0 -12
  440. package/dist/guest/toolchains/native.js +0 -220
  441. package/dist/guest/toolchains/scripting.d.ts +0 -10
  442. package/dist/guest/toolchains/scripting.js +0 -137
  443. package/dist/selftest/guest.test.d.ts +0 -3
  444. package/dist/selftest/guest.test.js +0 -61
  445. package/dist/selftest/helpers.test.d.ts +0 -2
  446. package/dist/selftest/helpers.test.js +0 -125
  447. package/dist/selftest/host.test.d.ts +0 -2
  448. package/dist/selftest/host.test.js +0 -71
  449. package/dist/selftest/live.test.d.ts +0 -7
  450. package/dist/selftest/live.test.js +0 -127
  451. package/dist/selftest/security.test.d.ts +0 -3
  452. package/dist/selftest/security.test.js +0 -143
  453. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -1,71 +0,0 @@
1
- // Runs every command and shortcut binary as a separate process in a simulated HOST: none of the sandbox-only folders
2
- // or variables exist (C:\AgentLaunch, C:\AgentData, CLAUDE_CONFIG_DIR, ...), with a fresh home and storage. Tests
3
- // usually run inside a sandbox, where those exist, so this is what catches code that only works there. Every command
4
- // must either succeed or fail with a friendly "Error:" message; a crash or stack trace fails.
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { spawnSync } from 'node:child_process';
8
- import { ensureDir } from '../core/util.js';
9
- const SANDBOX_ONLY_VARS = ['CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'GEMINI_CLI_HOME', 'AGENT_WORKSPACE', 'AGENT_DATA', 'AGENT_TOOLS', 'AGENT_PROJECT', 'NVM_HOME', 'NVM_SYMLINK', 'BASH_ENV', 'CLAUDE_CODE_GIT_BASH_PATH', 'AGENT_GIT_ACCESS', 'AGENT_REAL_GIT', 'AGENT_SANDBOX_CONFIG'];
10
- export const hostTests = (t) => {
11
- t.section('simulated host: every command, outside a sandbox');
12
- const home = path.join(t.temp, 'host-sim-home');
13
- const project = path.join(t.temp, 'host-sim-project');
14
- const storage = path.join(t.temp, 'host-sim-storage');
15
- for (const dir of [home, project, storage])
16
- ensureDir(dir);
17
- const config = t.write(path.join(t.temp, 'host-sim-config.json'), JSON.stringify({ storage: { root: storage }, personalization: { enabled: false }, provisioning: { terminal: { enabled: false } } }));
18
- const env = {
19
- ...Object.fromEntries(Object.entries(process.env).filter(([k]) => !SANDBOX_ONLY_VARS.includes(k))),
20
- AGENT_SANDBOX_HOME: home,
21
- AGENT_LAUNCH_DIR: path.join(t.temp, 'no-agent-launch'),
22
- AGENT_DATA_DIR: path.join(t.temp, 'no-agent-data'),
23
- AGENT_HOST_TOOLS_DIR: path.join(t.temp, 'no-host-tools'),
24
- NO_COLOR: '1',
25
- // This test may itself run inside a sandbox: the CLI must behave as it would on a PC.
26
- AGENT_SANDBOX_HOST_ONLY: '1',
27
- };
28
- const bin = (name) => path.join(t.packageRoot, 'dist', 'bin', name + '.js');
29
- const runs = [
30
- ['agent-sandbox', '--help', ['--help']],
31
- ['agent-sandbox', 'status', ['status', '--config', config]],
32
- ['agent-sandbox', 'status --json', ['status', '--json', '--config', config]],
33
- ['agent-sandbox', 'sessions', ['sessions', '--config', config]],
34
- ['agent-sandbox', 'doctor', ['doctor', '--config', config]],
35
- ['agent-sandbox', 'account list', ['account', 'list', '--config', config]],
36
- ['agent-sandbox', 'account use nobody', ['account', 'use', 'nobody', '--config', config]],
37
- ['agent-sandbox', 'config path', ['config', 'path']],
38
- ['agent-sandbox', 'config show', ['config', 'show', '--config', config]],
39
- ['agent-sandbox', 'config schema', ['config', 'schema']],
40
- ['agent-sandbox', 'config init', ['config', 'init']],
41
- ['agent-sandbox', 'config init (again)', ['config', 'init']],
42
- ['agent-sandbox', 'clean', ['clean', '--config', config]],
43
- ['agent-sandbox', 'sync', ['sync', '--config', config]],
44
- ['agent-sandbox', 'logs', ['logs', '--config', config]],
45
- ['agent-sandbox', 'attach', ['attach', '--config', config]],
46
- ['agent-sandbox', 'exec', ['exec', '--config', config, '--', 'echo', 'hi']],
47
- ['agent-sandbox', 'restart', ['restart', '--config', config]],
48
- ['agent-sandbox', 'stop', ['stop', '--config', config]],
49
- ['agent-sandbox', 'show', ['show', '--config', config]],
50
- ['agent-sandbox', 'hide', ['hide', '--config', config]],
51
- ['agent-sandbox', 'generate', ['generate', '--config', config]],
52
- ['agent-sandbox', 'launch', ['launch', '--config', config]],
53
- ['agent-sandbox', 'unknown command', ['frobnicate']],
54
- ['claude-sbx', 'claude-sbx --headless --continue', ['--headless', '--continue', '--config', config]],
55
- ['codex-sbx', 'codex-sbx', ['--config', config]],
56
- ['gemini-sbx', 'gemini-sbx', ['--config', config]],
57
- ['sbx-show', 'sbx-show', ['--config', config]],
58
- ['claude-host', 'claude-host --version', ['--version']],
59
- ];
60
- const broken = [];
61
- for (const [name, label, args] of runs) {
62
- const r = spawnSync(process.execPath, [bin(name), ...args], { cwd: project, env, encoding: 'utf8', timeout: 120000 });
63
- const output = ((r.stdout ?? '') + (r.stderr ?? '')).replace(/\x1b\[[0-9;]*m/g, '');
64
- const crashed = r.status === null || r.status > 1 || /Unexpected error|node:internal|^\s+at .+:\d+:\d+\)?$/m.test(output);
65
- const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(output) && name !== 'claude-host';
66
- if (crashed || unexplained)
67
- broken.push(label + ' (exit ' + r.status + '): ' + output.trim().split('\n').slice(0, 4).join(' | '));
68
- }
69
- t.check(broken.length === 0, 'all ' + runs.length + ' commands work or fail with a clear message when run outside a sandbox' + (broken.length ? ':\n ' + broken.join('\n ') : ''));
70
- t.check(fs.existsSync(path.join(home, 'config.json')) && fs.existsSync(path.join(home, 'config.schema.json')) && fs.existsSync(path.join(home, 'sandbox-config.schema.json')), 'on a fresh PC, config init and the local schema files are created');
71
- };
@@ -1,7 +0,0 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const liveTestsAvailable: () => boolean;
4
- /** Drives the real daemon through the run folder exactly like the host does, with cmd.exe standing in for claude. */
5
- export declare const daemonTests: (t: Harness, f: Fixtures, launchPath: string) => Promise<void>;
6
- /** Compiles gitshim.cs and runs real git through it in a scratch repository, as git.exe and through Git Bash. */
7
- export declare const gitGuardTests: (t: Harness, f: Fixtures, launchPath: string) => Promise<void>;
@@ -1,127 +0,0 @@
1
- // Live tests on Windows: the real guest session daemon + ptyhost (ConPTY), and the compiled git guard on a real repo.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { spawn, spawnSync } from 'node:child_process';
5
- import { ensureDir, findOnPath, sleep } from '../core/util.js';
6
- import { connect, encodeFrame, fixWindowsKeys, FrameReader, FrameType, helloFrame, queryViewers } from '../shared/pty-client.js';
7
- import { readJsonFile, writeJsonAtomic } from '../shared/rundir.js';
8
- const CSC = path.join(process.env.WINDIR ?? 'C:\\Windows', 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
9
- export const liveTestsAvailable = () => process.platform === 'win32' && fs.existsSync(CSC);
10
- /** Drives the real daemon through the run folder exactly like the host does, with cmd.exe standing in for claude. */
11
- export const daemonTests = async (t, f, launchPath) => {
12
- const { check, waitFor } = t;
13
- t.section('live: session daemon + ptyhost');
14
- const data = f.at('live-data');
15
- const workspace = f.workA;
16
- ensureDir(path.join(data, 'tools', 'agents', 'claude'));
17
- fs.copyFileSync(path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32', 'cmd.exe'), path.join(data, 'tools', 'agents', 'claude', 'claude.exe'));
18
- const sessionFile = path.join(launchPath, 'session.json');
19
- const session = f.readJson(sessionFile);
20
- Object.assign(session, { workspace, attach: { enabled: true } });
21
- Object.assign(session.harness, { arguments: ['/d', '/k', 'echo LIVE-AGENT-READY'], resumeArguments: ['/d', '/k', 'echo LIVE-AGENT-RESUMED'] });
22
- fs.writeFileSync(sessionFile, JSON.stringify(session));
23
- const env = { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_WORKSPACE: workspace, AGENT_LOCAL_DIR: f.at('local'), AGENT_LOG_PREFIX: 'daemon', AGENT_NO_TABS: '1' };
24
- const daemon = spawn(process.execPath, [path.join(launchPath, 'runtime', 'daemon.js')], { env, stdio: 'ignore', windowsHide: true });
25
- const run = path.join(data, 'run');
26
- const request = (body) => writeJsonAtomic(path.join(run, 'requests', body.id + '.json'), { createdAt: new Date().toISOString(), ...body });
27
- const response = (id) => waitFor(() => readJsonFile(path.join(run, 'responses', id + '.json')));
28
- try {
29
- request({ kind: 'session', id: 'live1', harness: 'claude', args: [], cwd: workspace });
30
- const s = await waitFor(() => { const v = readJsonFile(path.join(run, 'sessions', 'live1.json')); return v && v.phase !== 'starting' ? v : null; }, 60000);
31
- check(s?.phase === 'running' && Boolean(s.attach?.port), 'daemon compiles ptyhost and starts a session on request' + (s?.phase === 'failed' ? ' (' + s.message + ')' : ''));
32
- if (!s?.attach)
33
- return;
34
- const socket = await connect({ ...s.attach, addresses: ['127.0.0.1'] });
35
- const reader = new FrameReader();
36
- let out = '';
37
- let exitCode = null;
38
- socket.on('data', (c) => { for (const fr of reader.push(c)) {
39
- if (fr.type === FrameType.Output)
40
- out += fr.payload.toString();
41
- if (fr.type === FrameType.Exit)
42
- exitCode = fr.payload.readInt32LE(0);
43
- } });
44
- socket.write(helloFrame(s.attach.token, 100, 30));
45
- check(Boolean(await waitFor(() => out.includes('LIVE-AGENT-READY'), 10000)), 'an attached client sees the agent\'s screen');
46
- socket.write(encodeFrame(FrameType.Input, Buffer.from('echo TYPED-%USERNAME:~0,0%OK\r')));
47
- check(Boolean(await waitFor(() => out.includes('TYPED-OK'), 10000)), 'keystrokes reach the agent');
48
- const badClient = await connect({ ...s.attach, addresses: ['127.0.0.1'] });
49
- const badReader = new FrameReader();
50
- let denied = '';
51
- badClient.on('data', (c) => { for (const fr of badReader.push(c))
52
- if (fr.type === FrameType.Error)
53
- denied = fr.payload.toString(); });
54
- badClient.write(helloFrame('wrong-token', 80, 24));
55
- check(Boolean(await waitFor(() => denied, 20000)) && denied.includes('denied'), 'a client with the wrong token is refused');
56
- const local = { ...s.attach, addresses: ['127.0.0.1'] };
57
- const first = await queryViewers(local);
58
- const second = await connect(local);
59
- second.on('data', () => undefined);
60
- second.write(helloFrame(s.attach.token, 60, 20));
61
- const both = await waitFor(async () => ((await queryViewers(local))?.viewers === 2 ? true : null), 5000);
62
- second.destroy();
63
- const after = await waitFor(async () => { const v = await queryViewers(local); return v?.viewers === 1 ? v : null; }, 5000);
64
- const still = readJsonFile(path.join(run, 'sessions', 'live1.json'));
65
- check(first?.viewers === 1 && first.remote === 0 && Boolean(both) && after?.remote === 0 && still?.phase === 'running', 'viewers are counted live: a closed terminal stops counting at once and the session keeps running');
66
- check(await queryViewers({ ...local, token: 'wrong-token' }) === null, 'the viewer query needs the session token too');
67
- check(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])).equals(Buffer.from([0x61, 0x7f, 0x08, 0x1b])), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
68
- request({ kind: 'exec', id: 'exec1', command: ['echo', 'hello from exec'], cwd: workspace, timeoutSeconds: 30 });
69
- const ex = await response('exec1');
70
- check(Boolean(ex?.ok && ex.stdout?.includes('hello from exec')), 'exec runs a command in the sandbox and returns its output');
71
- request({ kind: 'exec', id: 'exec2', command: ['dir'], cwd: 'C:\\Windows', timeoutSeconds: 30 });
72
- const ex2 = await response('exec2');
73
- check(ex2?.ok === false && ex2.message.includes('not inside the sandbox workspace'), 'exec refuses folders outside the workspace');
74
- request({ kind: 'shell', id: 'evil', command: 'calc' });
75
- request({ kind: 'ping', id: 'ping1' });
76
- const ping = await response('ping1');
77
- check(Boolean(ping?.ok) && !fs.existsSync(path.join(run, 'responses', 'evil.json')) && !fs.existsSync(path.join(run, 'requests', 'evil.json')), 'ping answers; unknown request kinds are deleted unanswered');
78
- request({ kind: 'restart', id: 'restart1', harness: 'claude' });
79
- check(Boolean(await waitFor(() => out.includes('LIVE-AGENT-RESUMED'), 15000)), 'restart relaunches the agent with its resume arguments while attached');
80
- socket.write(encodeFrame(FrameType.Input, Buffer.from('exit 3\r')));
81
- await waitFor(() => exitCode !== null, 10000);
82
- const ended = await waitFor(() => { const v = readJsonFile(path.join(run, 'sessions', 'live1.json')); return v?.phase === 'exited' ? v : null; });
83
- check(exitCode === 3 && ended?.exitCode === 3, 'the agent\'s exit code reaches the client and the session status');
84
- socket.destroy();
85
- badClient.destroy();
86
- }
87
- finally {
88
- daemon.kill();
89
- await sleep(500);
90
- }
91
- };
92
- /** Compiles gitshim.cs and runs real git through it in a scratch repository, as git.exe and through Git Bash. */
93
- export const gitGuardTests = async (t, f, launchPath) => {
94
- const realGit = findOnPath(['git.exe']);
95
- t.section('live: git access guard');
96
- if (!realGit) {
97
- console.log('SKIP git guard (no git.exe on PATH)');
98
- return;
99
- }
100
- const shimDir = f.at('gitshim');
101
- ensureDir(shimDir);
102
- const compiled = spawnSync(CSC, ['/nologo', '/optimize', '/out:' + path.join(shimDir, 'git.exe'), path.join(launchPath, 'sandbox', 'gitshim.cs')], { encoding: 'utf8' });
103
- t.check(compiled.status === 0, 'gitshim.cs compiles with the .NET Framework compiler' + (compiled.status === 0 ? '' : ': ' + compiled.stdout));
104
- if (compiled.status !== 0)
105
- return;
106
- const repo = f.at('guard-repo');
107
- ensureDir(repo);
108
- spawnSync(realGit, ['init', '-q'], { cwd: repo });
109
- fs.writeFileSync(path.join(repo, 'a.txt'), 'hello');
110
- const guard = path.join(launchPath, 'runtime', 'gitguard.js');
111
- const env = (access) => ({ ...process.env, PATH: shimDir + path.delimiter + process.env.PATH, AGENT_GIT_ACCESS: access, AGENT_REAL_GIT: access === 'readonly' ? realGit : '', AGENT_GUARD_NODE: process.execPath, AGENT_GUARD_SCRIPT: guard, GIT_CONFIG_NOSYSTEM: '1' });
112
- const git = (access, args) => spawnSync('git', args, { cwd: repo, env: env(access), encoding: 'utf8' });
113
- const status = git('readonly', ['status', '--porcelain']);
114
- t.check(status.status === 0 && status.stdout.includes('a.txt'), 'readonly: `git status` works through the git.exe stand-in (exact arguments preserved)');
115
- const add = git('readonly', ['add', 'a.txt']);
116
- t.check(add.status === 128 && add.stderr.includes('read-only') && git('readonly', ['status', '--porcelain']).stdout.includes('??'), 'readonly: `git add` is refused and nothing changes');
117
- const none = git('none', ['status']);
118
- t.check(none.status === 128 && none.stderr.includes('disabled'), 'none: every git call is refused');
119
- const bash = [path.join(path.dirname(path.dirname(realGit)), 'bin', 'bash.exe'), path.join(path.dirname(path.dirname(realGit)), 'usr', 'bin', 'bash.exe')].find((b) => fs.existsSync(b));
120
- if (bash) {
121
- const bashEnv = t.write(f.at('gitshim', 'bashenv.sh'), 'git() { "' + process.execPath.replace(/\\/g, '/') + '" "' + guard.replace(/\\/g, '/') + '" "$@"; }\nexport -f git\n');
122
- const viaBash = spawnSync(bash, ['-c', 'git commit -m x'], { cwd: repo, env: { ...env('readonly'), BASH_ENV: bashEnv.replace(/\\/g, '/') }, encoding: 'utf8' });
123
- t.check(viaBash.status === 128 && viaBash.stderr.includes('read-only'), 'readonly: git inside Git Bash goes through the guard too (BASH_ENV)');
124
- }
125
- else
126
- console.log('SKIP Git Bash check (no bash.exe next to git)');
127
- };
@@ -1,3 +0,0 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const securityTests: (t: Harness, f: Fixtures) => Promise<void>;
@@ -1,143 +0,0 @@
1
- // Attacks a compromised sandbox could try against this PC, and the guards that stop them.
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { activateAccount, saveAccount } from '../host/accounts.js';
6
- import { isSandboxAddress, resetRunDir, sendRequest } from '../host/channel.js';
7
- import { pruneStorage } from '../host/cleanup.js';
8
- import { guardRepo, snapshotRepo } from '../host/repo-guard.js';
9
- import { assertNoLinks, ensureDir, safeText } from '../core/util.js';
10
- import { assertSafeMapping } from '../core/workspace.js';
11
- import { sandboxGlobalState } from '../host/claude-profile.js';
12
- import { findSecretFiles, stripMcpServers, stripSettings } from '../host/secrets.js';
13
- import { detectDatabases } from '../core/detect-databases.js';
14
- import { removeRefusal, safeRemove } from '../shared/safe-remove.js';
15
- import { gitDecision } from '../guest/gitguard.js';
16
- import { gitDenyRules } from '../guest/gitrules.js';
17
- import { encodeFrame, FrameReader, MAX_FRAME_BYTES, OutputFilter } from '../shared/pty-client.js';
18
- import { safeId } from '../shared/rundir.js';
19
- import { GuestRequest, SessionStatus } from '../shared/schema.js';
20
- import { syncHistoryIn } from '../sync/history.js';
21
- export const securityTests = async (t, f) => {
22
- const { check, rejects, write } = t;
23
- const at = f.at;
24
- t.section('links planted by the sandbox');
25
- const victim = at('victim');
26
- write(path.join(victim, 'precious.txt'), 'keep me');
27
- const data = at('evil-storage', 'data', 'd');
28
- ensureDir(path.join(data, 'state'));
29
- fs.symlinkSync(victim, path.join(data, 'state', 'claude'), 'junction');
30
- check(await rejects(() => assertNoLinks(data, path.join(data, 'state', 'claude', 'x.json')), 'is a link'), 'any host write under the data folder through a junction is refused');
31
- check(await rejects(() => activateAccount(at('evil-storage'), data, 'any'), 'Unknown account') && await rejects(() => saveAccount(at('evil-storage'), data, 'a', null), 'is a link'), 'accounts never read or write through a sandbox junction');
32
- const guestDot = at('link-guest');
33
- ensureDir(path.join(guestDot, 'projects'));
34
- fs.symlinkSync(victim, path.join(guestDot, 'projects', 'C--Workspace-my-app'), 'junction');
35
- write(at('link-host', 'projects', 'D--Code-my-app', 's.jsonl'), '{}\n');
36
- syncHistoryIn({ hostClaudeDir: at('link-host'), guestClaudeDir: guestDot, hostWorkspace: 'D:\\Code\\my-app', guestWorkspace: 'C:\\Workspace\\my-app' });
37
- check(!fs.existsSync(path.join(victim, 's.jsonl')), 'history sync never writes through a junction inside the sandbox state');
38
- fs.symlinkSync(path.join(victim, 'precious.txt'), path.join(guestDot, 'history.jsonl'), 'file');
39
- write(at('link-host', 'history.jsonl'), '{"display":"x","timestamp":1}\n');
40
- syncHistoryIn({ hostClaudeDir: at('link-host'), guestClaudeDir: guestDot, hostWorkspace: 'D:\\Code\\my-app', guestWorkspace: 'C:\\Workspace\\my-app' });
41
- check(fs.readFileSync(path.join(victim, 'precious.txt'), 'utf8') === 'keep me', 'a file symlink planted in place of history.jsonl is replaced, not written through');
42
- ensureDir(path.join(data, 'run'));
43
- fs.symlinkSync(victim, path.join(data, 'run', 'requests'), 'junction');
44
- check(await rejects(() => sendRequest(data, { kind: 'ping', id: 'p1', createdAt: '' }), 'is a link') && !fs.existsSync(path.join(victim, 'p1.json')), 'requests are never written through a junction');
45
- fs.symlinkSync(victim, path.join(data, 'run', 'sessions'), 'junction');
46
- resetRunDir(data);
47
- check(fs.existsSync(path.join(victim, 'precious.txt')) && !fs.existsSync(path.join(data, 'run', 'sessions')), 'resetting the run folder removes a junction itself, never its target');
48
- fs.symlinkSync(victim, path.join(data, 'logs'), 'junction');
49
- const old = new Date(Date.now() - 60 * 86400000);
50
- fs.utimesSync(path.join(victim, 'precious.txt'), old, old);
51
- pruneStorage(at('evil-storage'));
52
- check(fs.existsSync(path.join(victim, 'precious.txt')), 'cleanup never walks into a junction (old files on this PC stay)');
53
- t.section('host <-> sandbox channel');
54
- const validRequest = (r) => GuestRequest.safeParse(r).success;
55
- const validSession = (s) => SessionStatus.safeParse(s).success;
56
- check(await rejects(() => safeId('../evil'), 'invalid id') && !validRequest({ kind: 'shell', id: 'a' }) && !validRequest({ kind: 'exec', id: 'a/b', command: ['x'], cwd: 'C:\\', timeoutSeconds: 1 }) && validRequest({ kind: 'ping', id: 'abc', createdAt: 'x' }) && !validRequest({ kind: 'ping', id: 'abc', createdAt: 'x', extra: 1 }), 'only known request kinds with safe ids are accepted');
57
- const goodSession = { id: 's1', launchId: 'l', harness: 'claude', cwd: 'C:\\Workspace', message: 'ok', phase: 'running', updatedAt: 'x', startedAt: 'x', attach: { addresses: ['172.20.1.5'], port: 5000, token: 't' } };
58
- check(validSession(goodSession) && !validSession({ ...goodSession, attach: { ...goodSession.attach, port: 70000 } }) && !validSession({ ...goodSession, phase: 'pwned' }), 'guest session status is shape-checked');
59
- const ifaces = { 'vEthernet (Default Switch)': [{ address: '172.20.0.1', netmask: '255.255.240.0', family: 'IPv4', internal: false, mac: '', cidr: null }], Ethernet: [{ address: '192.168.1.10', netmask: '255.255.255.0', family: 'IPv4', internal: false, mac: '', cidr: null }] };
60
- check(isSandboxAddress('172.20.5.9', ifaces) && !isSandboxAddress('192.168.1.20', ifaces) && !isSandboxAddress('127.0.0.1', ifaces) && !isSandboxAddress('8.8.8.8', ifaces) && !isSandboxAddress('172.20.0.1', ifaces), 'attach only connects into the sandbox\'s Hyper-V subnet (not LAN, loopback, internet or this PC itself)');
61
- const filter = new OutputFilter();
62
- const filtered = Buffer.concat([filter.push(Buffer.from('a\x1b]52;c;ZXZpbA==\x07b\x1b]0;title\x07')), filter.push(Buffer.from('c\x1b]5')), filter.push(Buffer.from('2;c;cGF5\x1b\\d'))]).toString('latin1');
63
- check(filtered === 'a' + 'b\x1b]0;title\x07' + 'c' + 'd', 'clipboard writes (OSC 52) from the sandbox are stripped, even split across chunks; titles pass');
64
- check(await rejects(() => new FrameReader().push(Buffer.concat([Buffer.from([16]), Buffer.from([0, 0, 0, 0x7f])])), 'frame too large') && new FrameReader().push(encodeFrame(16, Buffer.alloc(MAX_FRAME_BYTES))).length === 1, 'oversized frames are rejected instead of buffered');
65
- t.section('git access modes');
66
- const allowed = (args, mode = 'readonly') => gitDecision(args, mode).allow;
67
- check(['status', 'log --oneline', 'diff HEAD~1', 'show HEAD', 'blame a.ts', 'branch', 'branch -a', 'branch --contains HEAD', 'tag', 'tag -l v*', 'remote -v', 'config --get user.name', 'stash list', '-C sub status', '--no-pager log'].every((a) => allowed(a.split(' '))), 'readonly: reading commands are allowed');
68
- check(['commit -m x', 'push', 'add .', 'checkout main', 'switch -c x', 'reset --hard', 'stash', 'stash pop', 'branch new', 'branch -D x', 'tag v1', 'merge x', 'rebase main', 'fetch', 'pull', 'config user.name x', 'remote add o u', '-c core.hooksPath=x status', '--git-dir=x status', 'clean -fd', 'apply p.diff'].every((a) => !allowed(a.split(' '))), 'readonly: anything that writes (or overrides config) is refused');
69
- check(!allowed(['status'], 'none') && !allowed(['--version'], 'none') && allowed(['commit'], 'full'), 'none: every git call is refused; full: everything allowed');
70
- const roRules = gitDenyRules('readonly');
71
- const noneRules = gitDenyRules('none');
72
- check(gitDenyRules('full').length === 0 && roRules.includes('Bash(git push:*)') && !roRules.some((r) => r.startsWith('Read(')) && noneRules.includes('Bash(git:*)') && noneRules.includes('Read(**/.git/**)'), 'Claude managed deny rules match each mode');
73
- t.section('repository guard (zero-click code execution on this PC)');
74
- const repo = at('repo');
75
- write(path.join(repo, '.git', 'config'), '[core]\n\trepositoryformatversion = 0\n[remote "origin"]\n\turl = https://example.com/x.git\n');
76
- write(path.join(repo, '.git', 'hooks', 'pre-commit.sample'), 'sample');
77
- write(path.join(repo, '.git', 'hooks', 'pre-push'), 'existing hook');
78
- const before = snapshotRepo(repo);
79
- write(path.join(repo, '.git', 'hooks', 'post-checkout'), 'calc.exe');
80
- write(path.join(repo, '.git', 'hooks', 'pre-push'), 'changed hook');
81
- fs.appendFileSync(path.join(repo, '.git', 'config'), '[core]\n\tfsmonitor = calc.exe\n\tsshCommand = evil\n[include]\n\tpath = C:/evil.cfg\n[branch "feature"]\n\tremote = origin\n');
82
- const findings = guardRepo(repo, before);
83
- const config = fs.readFileSync(path.join(repo, '.git', 'config'), 'utf8');
84
- check(!fs.existsSync(path.join(repo, '.git', 'hooks', 'post-checkout')) && !fs.existsSync(path.join(repo, '.git', 'hooks', 'pre-push')) && fs.existsSync(path.join(repo, '.git', 'hooks', 'pre-commit.sample')) && findings.filter((x) => x.kind === 'hook').length === 2, 'new or changed git hooks are quarantined (samples untouched)');
85
- check(/# \[agent-sandbox\] disabled.*fsmonitor/.test(config) && /# \[agent-sandbox\] disabled.*sshCommand/.test(config) && /# \[agent-sandbox\] disabled.*path = C:\/evil/.test(config) && /^\tremote = origin$/m.test(config) && /^\turl = https/m.test(config), 'dangerous git config added in the sandbox is disabled; harmless changes and existing lines stay');
86
- check(guardRepo(repo, before).length === 0, 'the guard is idempotent');
87
- const repo2 = at('repo2');
88
- write(path.join(repo2, '.git', 'config'), '[core]\n\tbare = false\n');
89
- write(path.join(repo2, 'build.cmd'), 'echo legit');
90
- const before2 = snapshotRepo(repo2);
91
- write(path.join(repo2, 'node.exe'), 'MZ');
92
- write(path.join(repo2, 'git.cmd'), 'calc');
93
- write(path.join(repo2, 'sandbox.cmd'), 'calc');
94
- write(path.join(repo2, 'opencode.ps1'), 'calc');
95
- write(path.join(repo2, '.git', 'commondir'), 'C:/evil');
96
- fs.appendFileSync(path.join(repo2, '.git', 'config'), '[core] fsmonitor = calc.exe\n');
97
- const found2 = guardRepo(repo2, before2);
98
- check(!fs.existsSync(path.join(repo2, 'node.exe')) && !fs.existsSync(path.join(repo2, 'git.cmd')) && fs.existsSync(path.join(repo2, 'build.cmd')) && !fs.existsSync(path.join(repo2, 'sandbox.cmd')) && found2.filter((x) => x.kind === 'command').length === 4, 'executables planted under command names (node.exe, git.cmd, sandbox.cmd, opencode.ps1) are quarantined; other scripts stay');
99
- check(!fs.existsSync(path.join(repo2, '.git', 'commondir')) && /# \[agent-sandbox\] disabled.*fsmonitor/.test(fs.readFileSync(path.join(repo2, '.git', 'config'), 'utf8')), 'a planted commondir and a key on the same line as its [section] are neutralised');
100
- const repo3 = at('repo3');
101
- write(path.join(repo3, '.git', 'config'), '[core]\n');
102
- const before3 = snapshotRepo(repo3);
103
- fs.rmSync(path.join(repo3, '.git'), { recursive: true });
104
- fs.symlinkSync(victim, path.join(repo3, '.git'), 'junction');
105
- check(guardRepo(repo3, before3).some((x) => x.kind === 'git' && x.detail.includes('replaced')), '.git replaced by a link is reported loudly');
106
- t.section('what reaches the sandbox, and what reaches your terminal');
107
- const state = sandboxGlobalState({ primaryApiKey: 'sk-secret', oauthAccount: { emailAddress: 'me' }, mcpServers: { a: {} }, theme: 'dark', projects: { 'D:/Code/app': { mcpServers: { db: { env: { TOKEN: 'mine' } } } }, 'D:/Other': { mcpServers: { x: { env: { TOKEN: 'other' } } } } } }, 'D:\\Code\\app');
108
- check(!('primaryApiKey' in state) && !('oauthAccount' in state) && JSON.stringify(state).includes('mine') && !JSON.stringify(state).includes('other') && state.theme === 'dark', 'the sandbox gets your MCP servers and this project, never API keys, your account or other projects');
109
- const secrets = { strip: true, allow: ['AWS_REGION'], patterns: ['ACME_*'], warnFiles: true };
110
- const removed = [];
111
- const settings = stripSettings({ apiKeyHelper: 'aws-vault exec prod -- get-key', model: 'opus', env: {
112
- AWS_ACCESS_KEY_ID: 'x', AWS_SECRET_ACCESS_KEY: 'y', AWS_REGION: 'eu-west-2', AZURE_CLIENT_SECRET: 'z', ARM_CLIENT_ID: 'id', CLOUDFLARE_API_TOKEN: 't',
113
- GOOGLE_APPLICATION_CREDENTIALS: 'C:\\key.json', PROD_DB_HOST: 'db.internal', STRIPE_KEY: 'k', ACME_THING: 'a', DATABASE_URL: 'postgres://u:p@h/db',
114
- INNOCENT: 'AKIAABCDEFGHIJKLMNOP', SAFE_URL: 'https://example.com', DISABLE_TELEMETRY: '1', CLAUDE_CODE_USE_BEDROCK: '1',
115
- } }, secrets, removed);
116
- const env = settings.env;
117
- check(!('apiKeyHelper' in settings) && settings.model === 'opus' && Object.keys(env).sort().join() === 'AWS_REGION,CLAUDE_CODE_USE_BEDROCK,DISABLE_TELEMETRY,SAFE_URL' && removed.some((r) => r.endsWith('AWS_SECRET_ACCESS_KEY')), 'settings.json: AWS, Azure, Google, Cloudflare, prod, payment and database credentials (by name or by value) stay on this PC; secrets.allow and ordinary settings pass');
118
- const mcp = stripMcpServers({ mcpServers: { gh: { command: 'npx', args: ['server', '--api-key', 'plain', '--verbose', 'ghp_abcdefghijklmnopqrstuvwxyz0123456789'], env: { GITHUB_TOKEN: 'g', LOG_LEVEL: 'debug' } } },
119
- projects: { 'D:/app': { mcpServers: { api: { type: 'http', url: 'https://x', headers: { Authorization: 'Bearer abcdefghijklmnopqrstuvwxyz', Accept: 'json' } } } } } }, secrets, []);
120
- const gh = mcp.mcpServers.gh;
121
- const api = (mcp.projects['D:/app']).mcpServers.api;
122
- check(JSON.stringify(gh.args) === '["server","--api-key","[removed by agent-sandbox]","--verbose","[removed by agent-sandbox]"]' && JSON.stringify(gh.env) === '{"LOG_LEVEL":"debug"}' && JSON.stringify(api.headers) === '{"Accept":"json"}', 'MCP servers: token env vars, Authorization headers and key arguments are removed, the rest of the server works as before');
123
- check(JSON.stringify(stripSettings({ env: { AWS_SECRET_ACCESS_KEY: 'y' } }, { ...secrets, strip: false }, [])) === '{"env":{"AWS_SECRET_ACCESS_KEY":"y"}}', 'secrets.strip = false copies settings unchanged');
124
- const sneaky = stripMcpServers({ mcpServers: { r: { type: 'http', url: 'https://mcp.example.com/mcp?api_key=0123456789abcdef&region=eu', headers: { 'X-API-Key': 'k', 'x-auth-token': 't', Accept: 'json' }, args: ['--api-key=deadbeefdeadbeef', '--header', 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz', '--port=8080'] } } }, secrets, []);
125
- const r = sneaky.mcpServers.r;
126
- check(r.url === 'https://mcp.example.com/mcp?api_key=REMOVED&region=eu' && JSON.stringify(r.headers) === '{"Accept":"json"}' && r.args[0] === '[removed by agent-sandbox]' && r.args[2] === '[removed by agent-sandbox]' && r.args[3] === '--port=8080', 'MCP servers: X-API-Key headers, ?api_key= in URLs, --flag=value keys and "Authorization: Bearer" arguments are removed too');
127
- const evil = ['Server=a;Database=a;'.repeat(12000), 'services:\n x:\n image: ' + 'postgres'.repeat(20000) + '!\n', 'Host=a;'.repeat(30000)].map((text, i) => ({ path: ['.env', 'compose.yaml', 'appsettings.json'][i], text }));
128
- const started = Date.now();
129
- detectDatabases(evil);
130
- check(Date.now() - started < 2000, 'database detection stays fast on hostile workspace files (no catastrophic backtracking): ' + (Date.now() - started) + ' ms');
131
- const profile = os.homedir();
132
- const refused = [profile, path.dirname(profile), 'C:\\', 'C:\\Windows', '', 'relative\\folder', at('outside-root', 'x')].map((p) => removeRefusal(p, at('safe-root')));
133
- const allowedTarget = at('safe-root', 'tools', 'go');
134
- t.write(path.join(allowedTarget, 'go.exe'), 'x');
135
- safeRemove(allowedTarget, at('safe-root'));
136
- check(refused.every((r) => r !== null) && !fs.existsSync(allowedTarget), 'deletes can never reach your profile, a folder above it, a drive root, Windows, a relative path or anything outside their root');
137
- const ws = at('secret-ws');
138
- for (const file of ['.env', '.env.example', 'server.pem', 'README.md', 'api/credentials.json', 'node_modules/x/.env'])
139
- t.write(path.join(ws, file), 'x');
140
- check(findSecretFiles(ws).sort().join() === '.env,api\\credentials.json,server.pem', 'secret-looking workspace files are found for the launch warning (templates and dependencies ignored)');
141
- check(safeText('ok \x1b[31mred\x1b[0m \x1b]52;c;ZXZpbA==\x07\x1b]0;title\x07\x1b[2J\x07end') === 'ok \x1b[31mred\x1b[0m end', 'sandbox text printed on this PC keeps colours but loses every other escape sequence');
142
- check(await rejects(() => assertSafeMapping(path.join(t.packageRoot, 'dist'), t.packageRoot, at('storage')), 'overlaps') && await rejects(() => assertSafeMapping(at('storage', 'data'), t.packageRoot, at('storage')), 'overlaps'), 'extra mappings may not reach agent-sandbox\'s own folders');
143
- };