windows-development-sandbox 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -2
  4. package/config.example.json +349 -0
  5. package/dist/api.d.ts +31 -0
  6. package/dist/api.js +31 -0
  7. package/dist/bin/agent-sandbox.d.ts +2 -0
  8. package/dist/bin/agent-sandbox.js +3 -0
  9. package/dist/bin/claude-host.d.ts +2 -0
  10. package/dist/bin/claude-host.js +5 -0
  11. package/dist/bin/claude-sbx.d.ts +2 -0
  12. package/dist/bin/claude-sbx.js +3 -0
  13. package/dist/bin/codex-sbx.d.ts +2 -0
  14. package/dist/bin/codex-sbx.js +3 -0
  15. package/dist/bin/gemini-sbx.d.ts +2 -0
  16. package/dist/bin/gemini-sbx.js +3 -0
  17. package/dist/bin/sandbox.d.ts +2 -0
  18. package/dist/bin/sandbox.js +4 -0
  19. package/dist/bin/sbx-hide.d.ts +2 -0
  20. package/dist/bin/sbx-hide.js +3 -0
  21. package/dist/bin/sbx-show.d.ts +2 -0
  22. package/dist/bin/sbx-show.js +3 -0
  23. package/dist/cli/args.d.ts +52 -0
  24. package/dist/cli/args.js +143 -0
  25. package/dist/cli/colors.d.ts +24 -0
  26. package/dist/cli/colors.js +42 -0
  27. package/dist/cli/commands/accounts.d.ts +2 -0
  28. package/dist/cli/commands/accounts.js +52 -0
  29. package/dist/cli/commands/agent-commands.d.ts +6 -0
  30. package/dist/cli/commands/agent-commands.js +61 -0
  31. package/dist/cli/commands/context.d.ts +23 -0
  32. package/dist/cli/commands/context.js +57 -0
  33. package/dist/cli/commands/data/clean.d.ts +15 -0
  34. package/dist/cli/commands/data/clean.js +101 -0
  35. package/dist/cli/commands/data/collect.d.ts +11 -0
  36. package/dist/cli/commands/data/collect.js +54 -0
  37. package/dist/cli/commands/data/index.d.ts +4 -0
  38. package/dist/cli/commands/data/index.js +45 -0
  39. package/dist/cli/commands/data/remove.d.ts +2 -0
  40. package/dist/cli/commands/data/remove.js +30 -0
  41. package/dist/cli/commands/index.d.ts +10 -0
  42. package/dist/cli/commands/index.js +10 -0
  43. package/dist/cli/commands/launch.d.ts +29 -0
  44. package/dist/cli/commands/launch.js +333 -0
  45. package/dist/cli/commands/maintenance.d.ts +8 -0
  46. package/dist/cli/commands/maintenance.js +154 -0
  47. package/dist/cli/commands/project.d.ts +29 -0
  48. package/dist/cli/commands/project.js +63 -0
  49. package/dist/cli/commands/projects.d.ts +14 -0
  50. package/dist/cli/commands/projects.js +146 -0
  51. package/dist/cli/commands/repair/checks.d.ts +2 -0
  52. package/dist/cli/commands/repair/checks.js +170 -0
  53. package/dist/cli/commands/repair/finders.d.ts +30 -0
  54. package/dist/cli/commands/repair/finders.js +108 -0
  55. package/dist/cli/commands/repair/index.d.ts +12 -0
  56. package/dist/cli/commands/repair/index.js +61 -0
  57. package/dist/cli/commands/repair/types.d.ts +26 -0
  58. package/dist/cli/commands/repair/types.js +3 -0
  59. package/dist/cli/commands/restart.d.ts +6 -0
  60. package/dist/cli/commands/restart.js +85 -0
  61. package/dist/cli/commands/sessions.d.ts +12 -0
  62. package/dist/cli/commands/sessions.js +257 -0
  63. package/dist/cli/commands/setup/checks.d.ts +13 -0
  64. package/dist/cli/commands/setup/checks.js +71 -0
  65. package/dist/cli/commands/setup/facts.d.ts +31 -0
  66. package/dist/cli/commands/setup/facts.js +60 -0
  67. package/dist/cli/commands/setup/index.d.ts +2 -0
  68. package/dist/cli/commands/setup/index.js +43 -0
  69. package/dist/cli/commands/user-config.d.ts +8 -0
  70. package/dist/cli/commands/user-config.js +41 -0
  71. package/dist/cli/history.d.ts +11 -0
  72. package/dist/cli/history.js +21 -0
  73. package/dist/cli/index.d.ts +20 -0
  74. package/dist/cli/index.js +121 -0
  75. package/dist/cli/manifest.d.ts +103 -0
  76. package/dist/cli/manifest.js +107 -0
  77. package/dist/cli/menu-multi.d.ts +12 -0
  78. package/dist/cli/menu-multi.js +82 -0
  79. package/dist/cli/menu.d.ts +14 -0
  80. package/dist/cli/menu.js +290 -0
  81. package/dist/cli/progress.d.ts +9 -0
  82. package/dist/cli/progress.js +49 -0
  83. package/dist/cli/settings/edit.d.ts +13 -0
  84. package/dist/cli/settings/edit.js +19 -0
  85. package/dist/cli/settings/index.d.ts +8 -0
  86. package/dist/cli/settings/index.js +131 -0
  87. package/dist/cli/settings/prompts.d.ts +7 -0
  88. package/dist/cli/settings/prompts.js +48 -0
  89. package/dist/cli/settings/schema.d.ts +33 -0
  90. package/dist/cli/settings/schema.js +29 -0
  91. package/dist/cli/validate.d.ts +17 -0
  92. package/dist/cli/validate.js +89 -0
  93. package/dist/core/config.d.ts +17 -0
  94. package/dist/core/config.js +106 -0
  95. package/dist/core/detect-databases.d.ts +15 -0
  96. package/dist/core/detect-databases.js +147 -0
  97. package/dist/core/detect-pwsh.d.ts +18 -0
  98. package/dist/core/detect-pwsh.js +111 -0
  99. package/dist/core/detect-toolchains.d.ts +30 -0
  100. package/dist/core/detect-toolchains.js +58 -0
  101. package/dist/core/disk.d.ts +16 -0
  102. package/dist/core/disk.js +55 -0
  103. package/dist/core/local-config.d.ts +20 -0
  104. package/dist/core/local-config.js +77 -0
  105. package/dist/core/project-policy.d.ts +15 -0
  106. package/dist/core/project-policy.js +35 -0
  107. package/dist/core/prompt.d.ts +4 -0
  108. package/dist/core/prompt.js +15 -0
  109. package/dist/core/safe-paths.d.ts +4 -0
  110. package/dist/core/safe-paths.js +37 -0
  111. package/dist/core/schema/common.d.ts +17 -0
  112. package/dist/core/schema/common.js +15 -0
  113. package/dist/core/schema/config.d.ts +594 -0
  114. package/dist/core/schema/config.js +63 -0
  115. package/dist/core/schema/databases.d.ts +33 -0
  116. package/dist/core/schema/databases.js +35 -0
  117. package/dist/core/schema/harness.d.ts +12 -0
  118. package/dist/core/schema/harness.js +17 -0
  119. package/dist/core/schema/json-schema.d.ts +5 -0
  120. package/dist/core/schema/json-schema.js +25 -0
  121. package/dist/core/schema/profile.d.ts +27 -0
  122. package/dist/core/schema/profile.js +29 -0
  123. package/dist/core/schema/projects.d.ts +9 -0
  124. package/dist/core/schema/projects.js +11 -0
  125. package/dist/core/schema/sandbox.d.ts +50 -0
  126. package/dist/core/schema/sandbox.js +36 -0
  127. package/dist/core/schema/toolchains.d.ts +93 -0
  128. package/dist/core/schema/toolchains.js +83 -0
  129. package/dist/core/schema.d.ts +6 -0
  130. package/dist/core/schema.js +8 -0
  131. package/dist/core/types.d.ts +190 -0
  132. package/dist/core/types.js +1 -0
  133. package/dist/core/util.d.ts +60 -0
  134. package/dist/core/util.js +154 -0
  135. package/dist/core/workspace.d.ts +18 -0
  136. package/dist/core/workspace.js +128 -0
  137. package/dist/guest/boot-env.d.ts +4 -0
  138. package/dist/guest/boot-env.js +25 -0
  139. package/dist/guest/cmdline.d.ts +12 -0
  140. package/dist/guest/cmdline.js +23 -0
  141. package/dist/guest/daemon/exec.d.ts +5 -0
  142. package/dist/guest/daemon/exec.js +32 -0
  143. package/dist/guest/daemon/project.d.ts +14 -0
  144. package/dist/guest/daemon/project.js +132 -0
  145. package/dist/guest/daemon/session.d.ts +13 -0
  146. package/dist/guest/daemon/session.js +139 -0
  147. package/dist/guest/daemon/shutdown.d.ts +1 -0
  148. package/dist/guest/daemon/shutdown.js +21 -0
  149. package/dist/guest/daemon/state.d.ts +17 -0
  150. package/dist/guest/daemon/state.js +31 -0
  151. package/dist/guest/daemon.d.ts +1 -0
  152. package/dist/guest/daemon.js +53 -0
  153. package/dist/guest/db.d.ts +1 -0
  154. package/dist/guest/db.js +75 -0
  155. package/dist/guest/env.d.ts +51 -0
  156. package/dist/guest/env.js +101 -0
  157. package/dist/guest/gitguard.d.ts +7 -0
  158. package/dist/guest/gitguard.js +60 -0
  159. package/dist/guest/gitrules.d.ts +2 -0
  160. package/dist/guest/gitrules.js +11 -0
  161. package/dist/guest/guide.d.ts +16 -0
  162. package/dist/guest/guide.js +69 -0
  163. package/dist/guest/helpers.d.ts +3 -0
  164. package/dist/guest/helpers.js +26 -0
  165. package/dist/guest/main.d.ts +1 -0
  166. package/dist/guest/main.js +130 -0
  167. package/dist/guest/ports.d.ts +6 -0
  168. package/dist/guest/ports.js +22 -0
  169. package/dist/guest/project-context.d.ts +7 -0
  170. package/dist/guest/project-context.js +10 -0
  171. package/dist/guest/project-env.d.ts +20 -0
  172. package/dist/guest/project-env.js +20 -0
  173. package/dist/guest/project-status.d.ts +5 -0
  174. package/dist/guest/project-status.js +17 -0
  175. package/dist/guest/ptyhost.d.ts +22 -0
  176. package/dist/guest/ptyhost.js +36 -0
  177. package/dist/guest/setup/agents.d.ts +11 -0
  178. package/dist/guest/setup/agents.js +87 -0
  179. package/dist/guest/setup/claude.d.ts +21 -0
  180. package/dist/guest/setup/claude.js +146 -0
  181. package/dist/guest/setup/environment.d.ts +16 -0
  182. package/dist/guest/setup/environment.js +69 -0
  183. package/dist/guest/setup/hooks.d.ts +9 -0
  184. package/dist/guest/setup/hooks.js +50 -0
  185. package/dist/guest/setup/network.d.ts +10 -0
  186. package/dist/guest/setup/network.js +99 -0
  187. package/dist/guest/setup/policy.d.ts +3 -0
  188. package/dist/guest/setup/policy.js +18 -0
  189. package/dist/guest/setup/user.d.ts +7 -0
  190. package/dist/guest/setup/user.js +47 -0
  191. package/dist/guest/toolchains/catalogue.d.ts +55 -0
  192. package/dist/guest/toolchains/catalogue.js +31 -0
  193. package/dist/guest/toolchains/cli.d.ts +10 -0
  194. package/dist/guest/toolchains/cli.js +72 -0
  195. package/dist/guest/toolchains/common.d.ts +54 -0
  196. package/dist/guest/toolchains/common.js +147 -0
  197. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  198. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  199. package/dist/guest/toolchains/databases/index.d.ts +6 -0
  200. package/dist/guest/toolchains/databases/index.js +62 -0
  201. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  203. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  204. package/dist/guest/toolchains/databases/mysql.js +38 -0
  205. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  206. package/dist/guest/toolchains/databases/postgres.js +38 -0
  207. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  208. package/dist/guest/toolchains/databases/redis.js +16 -0
  209. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  210. package/dist/guest/toolchains/databases/server.js +52 -0
  211. package/dist/guest/toolchains/git.d.ts +3 -0
  212. package/dist/guest/toolchains/git.js +121 -0
  213. package/dist/guest/toolchains/go.d.ts +3 -0
  214. package/dist/guest/toolchains/go.js +33 -0
  215. package/dist/guest/toolchains/index.d.ts +17 -0
  216. package/dist/guest/toolchains/index.js +80 -0
  217. package/dist/guest/toolchains/java.d.ts +8 -0
  218. package/dist/guest/toolchains/java.js +96 -0
  219. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  220. package/dist/guest/toolchains/leftovers.js +26 -0
  221. package/dist/guest/toolchains/managers.d.ts +16 -0
  222. package/dist/guest/toolchains/managers.js +84 -0
  223. package/dist/guest/toolchains/native/binstall.d.ts +7 -0
  224. package/dist/guest/toolchains/native/binstall.js +52 -0
  225. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  226. package/dist/guest/toolchains/native/cpp.js +38 -0
  227. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  228. package/dist/guest/toolchains/native/dotnet.js +103 -0
  229. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  230. package/dist/guest/toolchains/native/mesa.js +46 -0
  231. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  232. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  233. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  234. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  235. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  236. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  237. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  238. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  239. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  240. package/dist/guest/toolchains/native/rust.js +45 -0
  241. package/dist/guest/toolchains/node.d.ts +3 -0
  242. package/dist/guest/toolchains/node.js +152 -0
  243. package/dist/guest/toolchains/plan.d.ts +22 -0
  244. package/dist/guest/toolchains/plan.js +96 -0
  245. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  246. package/dist/guest/toolchains/pwsh.js +14 -0
  247. package/dist/guest/toolchains/python.d.ts +3 -0
  248. package/dist/guest/toolchains/python.js +54 -0
  249. package/dist/guest/toolchains/runtimes.d.ts +19 -0
  250. package/dist/guest/toolchains/runtimes.js +38 -0
  251. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  252. package/dist/guest/toolchains/scripting/php.js +71 -0
  253. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  254. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  255. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  256. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  257. package/dist/guest/toolchains/versions.d.ts +47 -0
  258. package/dist/guest/toolchains/versions.js +77 -0
  259. package/dist/guest/toolchains/vscode.d.ts +2 -0
  260. package/dist/guest/toolchains/vscode.js +53 -0
  261. package/dist/guest/view.d.ts +1 -0
  262. package/dist/guest/view.js +42 -0
  263. package/dist/host/accounts.d.ts +32 -0
  264. package/dist/host/accounts.js +128 -0
  265. package/dist/host/agent-commands.d.ts +25 -0
  266. package/dist/host/agent-commands.js +145 -0
  267. package/dist/host/attach.d.ts +17 -0
  268. package/dist/host/attach.js +69 -0
  269. package/dist/host/channel.d.ts +38 -0
  270. package/dist/host/channel.js +162 -0
  271. package/dist/host/claude-profile.d.ts +15 -0
  272. package/dist/host/claude-profile.js +107 -0
  273. package/dist/host/cleanup.d.ts +23 -0
  274. package/dist/host/cleanup.js +57 -0
  275. package/dist/host/database-plan.d.ts +12 -0
  276. package/dist/host/database-plan.js +52 -0
  277. package/dist/host/generate.d.ts +7 -0
  278. package/dist/host/generate.js +98 -0
  279. package/dist/host/join.d.ts +28 -0
  280. package/dist/host/join.js +76 -0
  281. package/dist/host/launch/mappings.d.ts +3 -0
  282. package/dist/host/launch/mappings.js +50 -0
  283. package/dist/host/launch/projects.d.ts +15 -0
  284. package/dist/host/launch/projects.js +56 -0
  285. package/dist/host/launch/stage.d.ts +11 -0
  286. package/dist/host/launch/stage.js +66 -0
  287. package/dist/host/launch/wsb.d.ts +10 -0
  288. package/dist/host/launch/wsb.js +34 -0
  289. package/dist/host/pc-scan.d.ts +6 -0
  290. package/dist/host/pc-scan.js +37 -0
  291. package/dist/host/personalize.d.ts +10 -0
  292. package/dist/host/personalize.js +97 -0
  293. package/dist/host/platform.d.ts +19 -0
  294. package/dist/host/platform.js +126 -0
  295. package/dist/host/project-scan.d.ts +7 -0
  296. package/dist/host/project-scan.js +89 -0
  297. package/dist/host/projects.d.ts +100 -0
  298. package/dist/host/projects.js +147 -0
  299. package/dist/host/repo-guard.d.ts +36 -0
  300. package/dist/host/repo-guard.js +219 -0
  301. package/dist/host/secrets.d.ts +14 -0
  302. package/dist/host/secrets.js +133 -0
  303. package/dist/host/terminal.d.ts +32 -0
  304. package/dist/host/terminal.js +130 -0
  305. package/dist/host/test-guard.d.ts +6 -0
  306. package/dist/host/test-guard.js +15 -0
  307. package/dist/host/window.d.ts +4 -0
  308. package/dist/host/window.js +72 -0
  309. package/dist/host/wsb.d.ts +37 -0
  310. package/dist/host/wsb.js +101 -0
  311. package/dist/runtime/daemon.js +28494 -0
  312. package/dist/runtime/db.js +26387 -0
  313. package/dist/runtime/gitguard.js +84 -0
  314. package/dist/runtime/main.js +28622 -0
  315. package/dist/runtime/view.js +25841 -0
  316. package/dist/selftest/accounts.test.d.ts +2 -0
  317. package/dist/selftest/accounts.test.js +44 -0
  318. package/dist/selftest/agent-commands.test.d.ts +2 -0
  319. package/dist/selftest/agent-commands.test.js +47 -0
  320. package/dist/selftest/channel.test.d.ts +2 -0
  321. package/dist/selftest/channel.test.js +37 -0
  322. package/dist/selftest/cli.test.d.ts +2 -0
  323. package/dist/selftest/cli.test.js +118 -0
  324. package/dist/selftest/cmdline.test.d.ts +2 -0
  325. package/dist/selftest/cmdline.test.js +45 -0
  326. package/dist/selftest/config.test.d.ts +2 -0
  327. package/dist/selftest/config.test.js +38 -0
  328. package/dist/selftest/data.test.d.ts +2 -0
  329. package/dist/selftest/data.test.js +80 -0
  330. package/dist/selftest/detect.test.d.ts +2 -0
  331. package/dist/selftest/detect.test.js +58 -0
  332. package/dist/selftest/fixtures.d.ts +272 -0
  333. package/dist/selftest/fixtures.js +80 -0
  334. package/dist/selftest/generate.test.d.ts +2 -0
  335. package/dist/selftest/generate.test.js +63 -0
  336. package/dist/selftest/git-access.test.d.ts +2 -0
  337. package/dist/selftest/git-access.test.js +26 -0
  338. package/dist/selftest/guest-boot.test.d.ts +2 -0
  339. package/dist/selftest/guest-boot.test.js +76 -0
  340. package/dist/selftest/harness.d.ts +51 -0
  341. package/dist/selftest/harness.js +77 -0
  342. package/dist/selftest/host-sim.test.d.ts +2 -0
  343. package/dist/selftest/host-sim.test.js +93 -0
  344. package/dist/selftest/hostile-input.test.d.ts +2 -0
  345. package/dist/selftest/hostile-input.test.js +54 -0
  346. package/dist/selftest/index.d.ts +8 -0
  347. package/dist/selftest/index.js +155 -0
  348. package/dist/selftest/links.test.d.ts +2 -0
  349. package/dist/selftest/links.test.js +50 -0
  350. package/dist/selftest/live-daemon.test.d.ts +2 -0
  351. package/dist/selftest/live-daemon.test.js +91 -0
  352. package/dist/selftest/live-git.test.d.ts +2 -0
  353. package/dist/selftest/live-git.test.js +60 -0
  354. package/dist/selftest/live-projects.test.d.ts +2 -0
  355. package/dist/selftest/live-projects.test.js +127 -0
  356. package/dist/selftest/live.d.ts +65 -0
  357. package/dist/selftest/live.js +109 -0
  358. package/dist/selftest/local-config.test.d.ts +2 -0
  359. package/dist/selftest/local-config.test.js +107 -0
  360. package/dist/selftest/mirror.test.d.ts +2 -0
  361. package/dist/selftest/mirror.test.js +101 -0
  362. package/dist/selftest/network.test.d.ts +2 -0
  363. package/dist/selftest/network.test.js +28 -0
  364. package/dist/selftest/personalization.test.d.ts +2 -0
  365. package/dist/selftest/personalization.test.js +53 -0
  366. package/dist/selftest/projects.test.d.ts +2 -0
  367. package/dist/selftest/projects.test.js +254 -0
  368. package/dist/selftest/repair.test.d.ts +2 -0
  369. package/dist/selftest/repair.test.js +42 -0
  370. package/dist/selftest/repo-guard.test.d.ts +2 -0
  371. package/dist/selftest/repo-guard.test.js +63 -0
  372. package/dist/selftest/secrets.test.d.ts +2 -0
  373. package/dist/selftest/secrets.test.js +71 -0
  374. package/dist/selftest/setup.test.d.ts +2 -0
  375. package/dist/selftest/setup.test.js +46 -0
  376. package/dist/selftest/suite.d.ts +13 -0
  377. package/dist/selftest/suite.js +1 -0
  378. package/dist/selftest/sync.test.d.ts +2 -0
  379. package/dist/selftest/sync.test.js +94 -0
  380. package/dist/selftest/test-guard.test.d.ts +2 -0
  381. package/dist/selftest/test-guard.test.js +49 -0
  382. package/dist/selftest/toolchains.test.d.ts +2 -0
  383. package/dist/selftest/toolchains.test.js +103 -0
  384. package/dist/selftest/workspace.test.d.ts +2 -0
  385. package/dist/selftest/workspace.test.js +53 -0
  386. package/dist/shared/download.d.ts +60 -0
  387. package/dist/shared/download.js +215 -0
  388. package/dist/shared/guest-paths.d.ts +30 -0
  389. package/dist/shared/guest-paths.js +61 -0
  390. package/dist/shared/netrules.d.ts +18 -0
  391. package/dist/shared/netrules.js +98 -0
  392. package/dist/shared/paths.d.ts +14 -0
  393. package/dist/shared/paths.js +28 -0
  394. package/dist/shared/projects.d.ts +53 -0
  395. package/dist/shared/projects.js +67 -0
  396. package/dist/shared/pty-client.d.ts +86 -0
  397. package/dist/shared/pty-client.js +227 -0
  398. package/dist/shared/rundir.d.ts +39 -0
  399. package/dist/shared/rundir.js +94 -0
  400. package/dist/shared/safe-remove.d.ts +10 -0
  401. package/dist/shared/safe-remove.js +66 -0
  402. package/dist/shared/schema.d.ts +527 -0
  403. package/dist/shared/schema.js +90 -0
  404. package/dist/shared/system.d.ts +15 -0
  405. package/dist/shared/system.js +19 -0
  406. package/dist/shared/test-guard.d.ts +4 -0
  407. package/dist/shared/test-guard.js +37 -0
  408. package/dist/sync/claude-mem.d.ts +8 -0
  409. package/dist/sync/claude-mem.js +79 -0
  410. package/dist/sync/history.d.ts +41 -0
  411. package/dist/sync/history.js +161 -0
  412. package/dist/sync/watcher.d.ts +21 -0
  413. package/dist/sync/watcher.js +135 -0
  414. package/hooks/post-agent/README.txt +1 -0
  415. package/hooks/pre-provision/README.txt +1 -0
  416. package/package.json +91 -4
  417. package/sandbox/bootstrap.ps1 +258 -0
  418. package/sandbox/cpucap.ps1 +51 -0
  419. package/sandbox/gitshim.cs +35 -0
  420. package/sandbox/logon.cmd +18 -0
  421. package/sandbox/openssl.cs +42 -0
  422. package/sandbox/ptyhost.cs +614 -0
  423. package/sandbox/trust.cmd +35 -0
  424. package/sandbox/vswhere.cs +83 -0
  425. package/schemas/config.schema.json +1161 -0
  426. package/schemas/sandbox-config.schema.json +968 -0
@@ -0,0 +1,77 @@
1
+ // Minimal test harness: PASS/FAIL lines (a failure shows what was expected and what was found), assertion helpers,
2
+ // and a temp folder that is always removed. Every test writes only below `t.temp`.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { inspect, isDeepStrictEqual } from 'node:util';
7
+ import { ensureDir, errMsg, sleep } from '../core/util.js';
8
+ const show = (value) => inspect(value, { depth: 6, breakLength: 110, maxStringLength: 600, maxArrayLength: 40 });
9
+ const indent = (text) => text.split('\n').map((line) => ' ' + line).join('\n');
10
+ const clip = (text, max = 800) => (text.length > max ? text.slice(0, max) + ' ... (' + text.length + ' characters)' : text);
11
+ export const createHarness = (packageRoot) => {
12
+ let failed = 0;
13
+ let total = 0;
14
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-sandbox-test-'));
15
+ const check = (ok, name, detail) => {
16
+ total++;
17
+ console.log((ok ? '\x1b[32mPASS\x1b[0m ' : '\x1b[31mFAIL\x1b[0m ') + name);
18
+ if (ok)
19
+ return;
20
+ failed++;
21
+ if (detail)
22
+ console.log(indent(detail));
23
+ };
24
+ /** Runs `fn` once; the message includes the `fix` hint of a UserError. */
25
+ const attempt = async (fn) => {
26
+ try {
27
+ await fn();
28
+ return { threw: false };
29
+ }
30
+ catch (e) {
31
+ return { threw: true, message: (errMsg(e) + ' ' + (e.fix ?? '')).trim() };
32
+ }
33
+ };
34
+ const thrownMessage = async (fn) => {
35
+ const outcome = await attempt(fn);
36
+ return outcome.threw ? outcome.message : '';
37
+ };
38
+ const write = (file, content) => { ensureDir(path.dirname(file)); fs.writeFileSync(file, content); return file; };
39
+ return {
40
+ packageRoot,
41
+ temp,
42
+ failures: () => failed,
43
+ checks: () => total,
44
+ check,
45
+ equal: (actual, expected, name) => check(isDeepStrictEqual(actual, expected), name, 'expected: ' + show(expected) + '\nactual: ' + show(actual)),
46
+ includes: (text, needle, name) => check(text.includes(needle), name, 'expected to contain: ' + show(needle) + '\nactual: ' + show(clip(text))),
47
+ excludes: (text, needle, name) => check(!text.includes(needle), name, 'expected NOT to contain: ' + show(needle) + '\nactual: ' + show(clip(text))),
48
+ matches: (text, pattern, name) => check(pattern.test(text), name, 'expected to match: ' + String(pattern) + '\nactual: ' + show(clip(text))),
49
+ throws: async (fn, expected, name) => {
50
+ const outcome = await attempt(fn);
51
+ const message = outcome.threw ? outcome.message : '';
52
+ const ok = outcome.threw && (typeof expected === 'string' ? message.includes(expected) : expected.test(message));
53
+ check(ok, name, 'expected an error containing: ' + String(expected) + '\n' + (outcome.threw ? 'actual error: ' + show(clip(message)) : 'but nothing was thrown'));
54
+ },
55
+ thrownMessage,
56
+ section: (title) => console.log('\n\x1b[36m-- ' + title + '\x1b[0m'),
57
+ skip: (what) => console.log('SKIP ' + what),
58
+ at: (...parts) => path.join(temp, ...parts),
59
+ dir: (...parts) => { const p = path.join(temp, ...parts); ensureDir(p); return p; },
60
+ write,
61
+ writeJson: (file, value) => write(file, JSON.stringify(value)),
62
+ readJson: (file) => JSON.parse(fs.readFileSync(file, 'utf8')),
63
+ touch: (file, secondsFromNow = 10) => { const when = new Date(Date.now() + secondsFromNow * 1000); fs.utimesSync(file, when, when); },
64
+ waitFor: async (read, ms = 30000, everyMs = 100) => {
65
+ const end = Date.now() + ms;
66
+ for (;;) {
67
+ const value = await read();
68
+ if (value)
69
+ return value;
70
+ if (Date.now() >= end)
71
+ return null;
72
+ await sleep(everyMs);
73
+ }
74
+ },
75
+ dispose: () => fs.rmSync(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 300 }),
76
+ };
77
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const hostSimSuite: Suite;
@@ -0,0 +1,93 @@
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
+ //
6
+ // Each command gets its own home and storage folder, so they cannot disturb each other and run several at a time
7
+ // (every one is a fresh Node process, which is what makes this the slowest suite when run one by one).
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ 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'];
11
+ const PARALLEL = 2;
12
+ const sandbox = (label, ...args) => ({ bin: 'agent-sandbox', label, args, config: true });
13
+ const COMMANDS = [
14
+ { bin: 'agent-sandbox', label: '--help', args: ['--help'] },
15
+ sandbox('status', 'status'),
16
+ sandbox('status --json', 'status', '--json'),
17
+ sandbox('sessions', 'sessions'),
18
+ sandbox('doctor', 'doctor'),
19
+ sandbox('account list', 'account', 'list'),
20
+ sandbox('account use nobody', 'account', 'use', 'nobody'),
21
+ { bin: 'agent-sandbox', label: 'config path', args: ['config', 'path'] },
22
+ sandbox('config show', 'config', 'show'),
23
+ { bin: 'agent-sandbox', label: 'config schema', args: ['config', 'schema'] },
24
+ sandbox('clean', 'clean'),
25
+ sandbox('sync', 'sync'),
26
+ sandbox('logs', 'logs'),
27
+ sandbox('attach', 'attach'),
28
+ { bin: 'agent-sandbox', label: 'exec', args: ['exec'], after: ['--', 'echo', 'hi'], config: true },
29
+ sandbox('restart', 'restart'),
30
+ sandbox('stop', 'stop'),
31
+ sandbox('show', 'show'),
32
+ sandbox('hide', 'hide'),
33
+ sandbox('generate', 'generate'),
34
+ sandbox('launch', 'launch'),
35
+ { bin: 'agent-sandbox', label: 'unknown command', args: ['frobnicate'] },
36
+ { bin: 'claude-sbx', label: 'claude-sbx --headless --continue', args: ['--headless', '--continue'], config: true },
37
+ { bin: 'codex-sbx', label: 'codex-sbx', args: [], config: true },
38
+ { bin: 'gemini-sbx', label: 'gemini-sbx', args: [], config: true },
39
+ { bin: 'sbx-show', label: 'sbx-show', args: [], config: true },
40
+ { bin: 'claude-host', label: 'claude-host --version', args: ['--version'] },
41
+ ];
42
+ export const hostSimSuite = {
43
+ name: 'host-sim',
44
+ title: 'every command run as its own process on a simulated host PC',
45
+ timeoutMs: 300000,
46
+ run: async (t, f) => {
47
+ t.section('every command outside a sandbox');
48
+ const baseEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => !SANDBOX_ONLY_VARS.includes(k)));
49
+ const project = t.dir('host-sim-project');
50
+ const envFor = (home) => ({
51
+ ...baseEnv,
52
+ AGENT_SANDBOX_HOME: home,
53
+ AGENT_LAUNCH_DIR: t.at('no-agent-launch'),
54
+ AGENT_DATA_DIR: t.at('no-agent-data'),
55
+ AGENT_HOST_TOOLS_DIR: t.at('no-host-tools'),
56
+ NO_COLOR: '1',
57
+ // This test may itself run inside a sandbox: the CLI must behave as it would on a PC.
58
+ AGENT_SANDBOX_HOST_ONLY: '1',
59
+ });
60
+ /** A fresh home, storage and config file for one command. */
61
+ const freshPc = (name) => {
62
+ const home = t.dir('host-sim', name, 'home');
63
+ const storage = t.dir('host-sim', name, 'storage');
64
+ const config = t.writeJson(t.at('host-sim', name, 'config.json'), { storage: { root: storage }, personalization: { enabled: false }, provisioning: { terminal: { enabled: false } } });
65
+ return { home, config };
66
+ };
67
+ const broken = [];
68
+ const runOne = async (command, index, pc = freshPc('c' + index)) => {
69
+ const args = command.config ? [...command.args, '--config', pc.config, ...(command.after ?? [])] : command.args;
70
+ const r = await f.runBin(command.bin, args, { cwd: project, env: envFor(pc.home), timeoutMs: 120000 });
71
+ const crashed = r.status === null || r.status > 1 || /Unexpected error|node:internal|^\s+at .+:\d+:\d+\)?$/m.test(r.out);
72
+ const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(r.out) && command.bin !== 'claude-host';
73
+ if (crashed || unexplained)
74
+ broken.push(command.label + ' (exit ' + r.status + (r.timedOut ? ', timed out' : '') + '): ' + r.out.trim().split('\n').slice(0, 4).join(' | '));
75
+ };
76
+ // `config init` twice must see the same home (the second run finds the files the first one made).
77
+ const initPc = freshPc('init');
78
+ const queue = COMMANDS.map((command, index) => ({ command, index }));
79
+ const workers = Array.from({ length: PARALLEL }, async () => {
80
+ for (let next = queue.shift(); next; next = queue.shift())
81
+ await runOne(next.command, next.index);
82
+ });
83
+ const initRuns = (async () => {
84
+ await runOne({ bin: 'agent-sandbox', label: 'config init', args: ['config', 'init'] }, 1000, initPc);
85
+ await runOne({ bin: 'agent-sandbox', label: 'config init (again)', args: ['config', 'init'] }, 1001, initPc);
86
+ })();
87
+ await Promise.all([...workers, initRuns]);
88
+ t.equal(broken, [], 'all ' + (COMMANDS.length + 2) + ' commands work or fail with a clear message when run outside a sandbox');
89
+ for (const file of ['config.json', 'config.schema.json', 'sandbox-config.schema.json']) {
90
+ t.check(fs.existsSync(path.join(initPc.home, file)), 'on a fresh PC, config init creates ' + file);
91
+ }
92
+ },
93
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const hostileInputSuite: Suite;
@@ -0,0 +1,54 @@
1
+ // Names, paths and text coming from a hostile sandbox or workspace: file names, deletes, printed text, mappings, state
2
+ // files and the launch path.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { assertCmdSafePath } from '../host/platform.js';
7
+ import { assertSafeMapping } from '../core/safe-paths.js';
8
+ import { safeText } from '../core/util.js';
9
+ import { isSafeFileName } from '../shared/paths.js';
10
+ import { removeRefusal, safeRemove } from '../shared/safe-remove.js';
11
+ import { writeJsonAtomic } from '../shared/rundir.js';
12
+ export const hostileInputSuite = {
13
+ name: 'hostile-input',
14
+ title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
15
+ run: async (t) => {
16
+ t.section('file names');
17
+ const unsafe = ['CON.jsonl', 'nul', 'aux.json', 'COM1.txt', 'a:b', 'x.jsonl.', 'x ', '..', 'a\\b', 'LPT3'];
18
+ t.equal(unsafe.filter(isSafeFileName), [], 'device names, streams and trailing dots are not valid names to copy into this PC');
19
+ const fine = ['C--Workspace-app', 'a.jsonl', '.credentials.json', 'console.jsonl'];
20
+ t.equal(fine.filter((name) => !isSafeFileName(name)), [], 'ordinary names (even ones that merely contain a device name) are valid');
21
+ t.section('deletes');
22
+ const profile = os.homedir();
23
+ const root = t.at('safe-root');
24
+ const targets = {
25
+ 'your profile': profile,
26
+ 'a folder above your profile': path.dirname(profile),
27
+ 'a drive root': 'C:\\',
28
+ Windows: 'C:\\Windows',
29
+ 'an empty path': '',
30
+ 'a relative path': 'relative\\folder',
31
+ 'a path outside the root': t.at('outside-root', 'x'),
32
+ };
33
+ for (const [what, target] of Object.entries(targets))
34
+ t.check(removeRefusal(target, root) !== null, 'a delete is refused for ' + what);
35
+ t.check(removeRefusal('\\\\?\\C:\\Users\\someone\\x', '\\\\?\\C:\\Users\\someone') !== null, 'a delete is refused for \\\\?\\ paths');
36
+ t.check(removeRefusal('C:\\Windows\\Temp\\x', 'C:\\Windows\\Temp') !== null, 'a delete is refused inside Windows');
37
+ t.check(removeRefusal('C:\\Program Files\\x\\y', 'C:\\Program Files\\x') !== null, 'a delete is refused inside Program Files');
38
+ const allowedTarget = path.join(root, 'tools', 'go');
39
+ t.write(path.join(allowedTarget, 'go.exe'), 'x');
40
+ safeRemove(allowedTarget, root);
41
+ t.check(!fs.existsSync(allowedTarget), 'a folder inside its root is deleted');
42
+ t.section('text, mappings, state files, launch path');
43
+ t.equal(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');
44
+ await t.throws(() => assertSafeMapping(path.join(t.packageRoot, 'dist'), t.packageRoot, t.at('storage')), 'overlaps', 'extra mappings may not reach the install folder');
45
+ await t.throws(() => assertSafeMapping(t.at('storage', 'data'), t.packageRoot, t.at('storage')), 'overlaps', 'extra mappings may not reach the storage folder');
46
+ const atomicFile = t.at('atomic', 'state.json');
47
+ writeJsonAtomic(atomicFile, { a: 1 });
48
+ writeJsonAtomic(atomicFile, { a: 2 });
49
+ t.equal(t.readJson(atomicFile).a, 2, 'state files are replaced whole...');
50
+ t.equal(fs.readdirSync(path.dirname(atomicFile)).length, 1, '... with no temp files left behind');
51
+ await t.throws(() => assertCmdSafePath('C:\\a\\%TEMP%\\agent.wsb'), 'cannot be started safely', 'a launch path with %VAR% (which cmd.exe would expand) is refused');
52
+ await t.throws(() => assertCmdSafePath('C:\\a"&calc&"\\agent.wsb'), 'cannot be started safely', 'a launch path with quotes and & is refused');
53
+ },
54
+ };
@@ -0,0 +1,8 @@
1
+ import type { Suite } from './suite.js';
2
+ /** Every suite, in the order they run. The live ones come last: they are the slowest and need Windows. */
3
+ export declare const SUITES: Suite[];
4
+ /** Windows Sandbox (its user is always WDAGUtilityAccount) or a GitHub Actions runner: never someone's PC. */
5
+ export declare const onThrowawayMachine: () => boolean;
6
+ /** Resolves "a,b" / ["a", "b"] to suites; throws with the list of valid names for an unknown one. */
7
+ export declare const pickSuites: (names: string[]) => Suite[];
8
+ export declare const selfTest: (packageRoot: string, suiteNames?: string[]) => Promise<number>;
@@ -0,0 +1,155 @@
1
+ // `agent-sandbox self-test [suite...]`: offline logic and security tests, plus live tests of the session daemon, ptyhost
2
+ // and the git guard when running on Windows (they need csc.exe and ConPTY, both part of Windows 10/11).
3
+ // They start real processes (the guest runtime, the daemon, csc, git), so they only run on a throwaway machine.
4
+ //
5
+ // Each area is one suite in its own *.test.ts file; the list below is the order they run in. Give suite names (or a
6
+ // comma-separated list) to run only those: `sandbox self-test config`, `npm test -- live-daemon`.
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { errMsg } from '../core/util.js';
10
+ import { createFixtures } from './fixtures.js';
11
+ import { createHarness } from './harness.js';
12
+ import { liveTestsAvailable } from './live.js';
13
+ import { accountsSuite } from './accounts.test.js';
14
+ import { agentCommandsSuite } from './agent-commands.test.js';
15
+ import { channelSuite } from './channel.test.js';
16
+ import { cliSuite } from './cli.test.js';
17
+ import { cmdlineSuite } from './cmdline.test.js';
18
+ import { configSuite } from './config.test.js';
19
+ import { dataSuite } from './data.test.js';
20
+ import { detectSuite } from './detect.test.js';
21
+ import { generateSuite } from './generate.test.js';
22
+ import { gitAccessSuite } from './git-access.test.js';
23
+ import { guestBootSuite } from './guest-boot.test.js';
24
+ import { hostileInputSuite } from './hostile-input.test.js';
25
+ import { hostSimSuite } from './host-sim.test.js';
26
+ import { linksSuite } from './links.test.js';
27
+ import { liveDaemonSuite } from './live-daemon.test.js';
28
+ import { liveGitSuite } from './live-git.test.js';
29
+ import { liveProjectsSuite } from './live-projects.test.js';
30
+ import { localConfigSuite } from './local-config.test.js';
31
+ import { mirrorSuite } from './mirror.test.js';
32
+ import { networkSuite } from './network.test.js';
33
+ import { personalizationSuite } from './personalization.test.js';
34
+ import { projectsSuite } from './projects.test.js';
35
+ import { repairSuite } from './repair.test.js';
36
+ import { repoGuardSuite } from './repo-guard.test.js';
37
+ import { secretsSuite } from './secrets.test.js';
38
+ import { setupSuite } from './setup.test.js';
39
+ import { syncSuite } from './sync.test.js';
40
+ import { testGuardSuite } from './test-guard.test.js';
41
+ import { toolchainsSuite } from './toolchains.test.js';
42
+ import { workspaceSuite } from './workspace.test.js';
43
+ /** Every suite, in the order they run. The live ones come last: they are the slowest and need Windows. */
44
+ export const SUITES = [
45
+ // settings and project files
46
+ configSuite, workspaceSuite, localConfigSuite,
47
+ // the generated sandbox and the guest that boots it
48
+ generateSuite, guestBootSuite, mirrorSuite, projectsSuite,
49
+ // syncing with this PC
50
+ syncSuite, accountsSuite,
51
+ // an untrusted sandbox: what the host refuses to do with what it finds
52
+ linksSuite, channelSuite, gitAccessSuite, repoGuardSuite, secretsSuite, hostileInputSuite, testGuardSuite,
53
+ // pure helpers and the command line
54
+ toolchainsSuite, detectSuite, networkSuite, personalizationSuite, cmdlineSuite, setupSuite, dataSuite, repairSuite, cliSuite, agentCommandsSuite,
55
+ // every command as its own process
56
+ hostSimSuite,
57
+ // real processes
58
+ liveDaemonSuite, liveProjectsSuite, liveGitSuite,
59
+ ];
60
+ /** Windows Sandbox (its user is always WDAGUtilityAccount) or a GitHub Actions runner: never someone's PC. */
61
+ export const onThrowawayMachine = () => os.userInfo().username.toLowerCase() === 'wdagutilityaccount' || process.env.GITHUB_ACTIONS === 'true';
62
+ const withDeadline = async (work, ms, what) => {
63
+ let timer;
64
+ const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(what + ' did not finish within ' + Math.round(ms / 1000) + ' s')), ms); });
65
+ try {
66
+ return await Promise.race([work, deadline]);
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ };
72
+ const runSuite = async (suite, t, f) => {
73
+ const checksBefore = t.checks();
74
+ const failedBefore = t.failures();
75
+ const started = Date.now();
76
+ let note;
77
+ if (suite.live && !liveTestsAvailable()) {
78
+ t.section(suite.name);
79
+ t.skip(suite.title + ' (needs Windows with the .NET Framework compiler)');
80
+ note = 'skipped';
81
+ }
82
+ else {
83
+ try {
84
+ await withDeadline(Promise.resolve(suite.run(t, f)), suite.timeoutMs ?? 120000, 'suite "' + suite.name + '"');
85
+ }
86
+ catch (e) {
87
+ t.check(false, 'suite "' + suite.name + '" stopped with an unexpected error', errMsg(e) + (e instanceof Error && e.stack ? '\n' + e.stack : ''));
88
+ }
89
+ }
90
+ return { suite, checks: t.checks() - checksBefore, failed: t.failures() - failedBefore, seconds: (Date.now() - started) / 1000, note };
91
+ };
92
+ const printSummary = (results) => {
93
+ const width = Math.max(...results.map((r) => r.suite.name.length));
94
+ console.log('\n\x1b[36m-- summary\x1b[0m');
95
+ for (const r of results) {
96
+ const state = r.note ?? (r.failed ? r.failed + ' FAILED' : 'ok');
97
+ console.log(' ' + r.suite.name.padEnd(width) + ' ' + String(r.checks).padStart(4) + ' checks ' + r.seconds.toFixed(1).padStart(6) + ' s ' + state);
98
+ }
99
+ const total = results.reduce((sum, r) => sum + r.checks, 0);
100
+ console.log(' ' + 'total'.padEnd(width) + ' ' + String(total).padStart(4) + ' checks ' + results.reduce((sum, r) => sum + r.seconds, 0).toFixed(1).padStart(6) + ' s');
101
+ };
102
+ /** Resolves "a,b" / ["a", "b"] to suites; throws with the list of valid names for an unknown one. */
103
+ export const pickSuites = (names) => {
104
+ const wanted = names.flatMap((n) => n.split(',')).map((n) => n.trim()).filter(Boolean);
105
+ if (wanted.length === 0)
106
+ return SUITES;
107
+ const unknown = wanted.filter((name) => !SUITES.some((s) => s.name === name));
108
+ if (unknown.length)
109
+ throw new Error('No test suite called ' + unknown.join(', ') + '. Suites: ' + SUITES.map((s) => s.name).join(', '));
110
+ return SUITES.filter((s) => wanted.includes(s.name));
111
+ };
112
+ export const selfTest = async (packageRoot, suiteNames = []) => {
113
+ if (!onThrowawayMachine()) {
114
+ console.error('The self-test only runs inside a Windows Sandbox, so it can never change this PC.\nRun it there: `sandbox start` in this folder, then `npm test` in the sandbox\'s terminal.');
115
+ return 1;
116
+ }
117
+ let chosen;
118
+ try {
119
+ chosen = pickSuites(suiteNames);
120
+ }
121
+ catch (e) {
122
+ console.error(errMsg(e));
123
+ return 1;
124
+ }
125
+ // Defence in depth (shared/test-guard.ts): with this set, host code refuses outward actions and writes outside temp.
126
+ process.env.AGENT_SANDBOX_SELFTEST = '1';
127
+ const t = createHarness(packageRoot);
128
+ const saved = { home: process.env.AGENT_SANDBOX_HOME, windir: process.env.WINDIR };
129
+ process.env.AGENT_SANDBOX_HOME = path.join(t.temp, 'user-home');
130
+ const results = [];
131
+ try {
132
+ const f = createFixtures(t);
133
+ for (const suite of chosen)
134
+ results.push(await runSuite(suite, t, f));
135
+ }
136
+ catch (e) {
137
+ t.check(false, 'unexpected error: ' + errMsg(e), e instanceof Error ? e.stack : undefined);
138
+ }
139
+ finally {
140
+ if (saved.home === undefined)
141
+ delete process.env.AGENT_SANDBOX_HOME;
142
+ else
143
+ process.env.AGENT_SANDBOX_HOME = saved.home;
144
+ if (saved.windir === undefined)
145
+ delete process.env.WINDIR;
146
+ else
147
+ process.env.WINDIR = saved.windir;
148
+ t.dispose();
149
+ }
150
+ if (results.length > 1)
151
+ printSummary(results);
152
+ const failures = t.failures();
153
+ console.log(failures === 0 ? '\nALL TESTS PASSED' : '\n' + failures + ' TEST(S) FAILED');
154
+ return failures === 0 ? 0 : 1;
155
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const linksSuite: Suite;
@@ -0,0 +1,50 @@
1
+ // Links (junctions and symlinks) planted by a compromised sandbox in the folders this PC reads and writes: the host
2
+ // must never write, delete or walk through them.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { activateAccount, saveAccount } from '../host/accounts.js';
6
+ import { resetRunDir, sendRequest } from '../host/channel.js';
7
+ import { pruneStorage } from '../host/cleanup.js';
8
+ import { assertNoLinks } from '../core/util.js';
9
+ import { syncHistoryIn } from '../sync/history.js';
10
+ export const linksSuite = {
11
+ name: 'links',
12
+ title: 'links planted by the sandbox are never followed',
13
+ run: async (t) => {
14
+ const victim = t.at('victim');
15
+ const precious = t.write(path.join(victim, 'precious.txt'), 'keep me');
16
+ const storage = t.at('evil-storage');
17
+ const data = t.dir('evil-storage', 'data', 'd');
18
+ t.dir('evil-storage', 'data', 'd', 'state');
19
+ t.section('writes under the data folder');
20
+ fs.symlinkSync(victim, path.join(data, 'state', 'claude'), 'junction');
21
+ await t.throws(() => assertNoLinks(data, path.join(data, 'state', 'claude', 'x.json')), 'is a link', 'any host write under the data folder through a junction is refused');
22
+ await t.throws(() => activateAccount(storage, data, 'any'), 'Unknown account', 'accounts never read through a sandbox junction');
23
+ await t.throws(() => saveAccount(storage, data, 'a', null), 'is a link', 'accounts never write through a sandbox junction');
24
+ t.section('history sync');
25
+ const sides = { hostClaudeDir: t.at('link-host'), guestClaudeDir: t.at('link-guest'), hostWorkspace: 'D:\\Code\\my-app', guestWorkspace: 'C:\\Workspace\\my-app' };
26
+ t.dir('link-guest', 'projects');
27
+ fs.symlinkSync(victim, path.join(sides.guestClaudeDir, 'projects', 'C--Workspace-my-app'), 'junction');
28
+ t.write(path.join(sides.hostClaudeDir, 'projects', 'D--Code-my-app', 's.jsonl'), '{}\n');
29
+ syncHistoryIn(sides);
30
+ t.check(!fs.existsSync(path.join(victim, 's.jsonl')), 'history sync never writes through a junction inside the sandbox state');
31
+ fs.symlinkSync(precious, path.join(sides.guestClaudeDir, 'history.jsonl'), 'file');
32
+ t.write(path.join(sides.hostClaudeDir, 'history.jsonl'), '{"display":"x","timestamp":1}\n');
33
+ syncHistoryIn(sides);
34
+ t.equal(fs.readFileSync(precious, 'utf8'), 'keep me', 'a file symlink planted in place of history.jsonl is replaced, not written through');
35
+ t.section('the run folder and cleanup');
36
+ t.dir('evil-storage', 'data', 'd', 'run');
37
+ fs.symlinkSync(victim, path.join(data, 'run', 'requests'), 'junction');
38
+ await t.throws(() => sendRequest(data, { kind: 'ping', id: 'p1', createdAt: '' }), 'is a link', 'requests are never written through a junction');
39
+ t.check(!fs.existsSync(path.join(victim, 'p1.json')), 'nothing was written into the junction\'s target');
40
+ fs.symlinkSync(victim, path.join(data, 'run', 'sessions'), 'junction');
41
+ resetRunDir(data);
42
+ t.check(!fs.existsSync(path.join(data, 'run', 'sessions')), 'resetting the run folder removes a junction itself...');
43
+ t.check(fs.existsSync(precious), '... never its target');
44
+ fs.symlinkSync(victim, path.join(data, 'logs'), 'junction');
45
+ const longAgo = new Date(Date.now() - 60 * 86400000);
46
+ fs.utimesSync(precious, longAgo, longAgo);
47
+ pruneStorage(storage);
48
+ t.check(fs.existsSync(precious), 'cleanup never walks into a junction (old files on this PC stay)');
49
+ },
50
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const liveDaemonSuite: Suite;
@@ -0,0 +1,91 @@
1
+ // Live: the real guest session daemon + ptyhost (ConPTY), driven through the run folder exactly like the host does, with
2
+ // cmd.exe standing in for the agent. One daemon serves all the checks below, in order (compiling ptyhost is slow).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { queryViewers } from '../shared/pty-client.js';
6
+ import { copyCmdAsFakeAgent, openViewer, sees, startDaemon, tail, viewersWhen, WAIT } from './live.js';
7
+ export const liveDaemonSuite = {
8
+ name: 'live-daemon',
9
+ title: 'live: session daemon + ptyhost (start, attach, viewers, exec, restart, exit)',
10
+ live: true,
11
+ timeoutMs: 360000,
12
+ run: async (t, f) => {
13
+ const gen = await f.launch();
14
+ const data = t.at('live-data');
15
+ const workspace = f.workA;
16
+ copyCmdAsFakeAgent(data);
17
+ const session = f.readJson(gen.sessionPath);
18
+ Object.assign(session, { workspace, attach: { enabled: true } });
19
+ Object.assign(session.harness, { arguments: ['/d', '/k', 'echo LIVE-AGENT-READY'], resumeArguments: ['/d', '/k', 'echo LIVE-AGENT-RESUMED'] });
20
+ t.writeJson(gen.sessionPath, session);
21
+ const daemon = startDaemon(t, f, { launchPath: gen.launchPath, data, workspace });
22
+ const viewers = [];
23
+ try {
24
+ t.section('live: starting a session');
25
+ daemon.request({ kind: 'session', id: 'live1', harness: 'claude', args: [], cwd: workspace });
26
+ const started = await daemon.started('live1');
27
+ const running = started?.phase === 'running' && Boolean(started.attach?.port);
28
+ t.check(running, 'the daemon compiles ptyhost and starts a session on request', 'phase: ' + started?.phase + ' ' + (started?.message ?? '(no status written)') + '\n' + daemon.diagnostics());
29
+ if (!started?.attach)
30
+ return;
31
+ const attach = started.attach;
32
+ const local = { ...attach, addresses: ['127.0.0.1'] };
33
+ t.section('live: attached clients');
34
+ const viewer = await openViewer(attach);
35
+ viewers.push(viewer);
36
+ const sawReady = await sees(t, viewer, 'LIVE-AGENT-READY');
37
+ t.check(Boolean(sawReady), 'an attached client sees the agent\'s screen', 'screen so far: ' + tail(viewer.output()) + '\n' + daemon.diagnostics());
38
+ viewer.type('echo TYPED-%USERNAME:~0,0%OK');
39
+ const sawTyped = await sees(t, viewer, 'TYPED-OK');
40
+ t.check(Boolean(sawTyped), 'keystrokes reach the agent', 'screen so far: ' + tail(viewer.output()));
41
+ const intruder = await openViewer(attach, { token: 'wrong-token', cols: 80, rows: 24 });
42
+ viewers.push(intruder);
43
+ const denied = await t.waitFor(() => intruder.error(), WAIT.viewers);
44
+ t.check(Boolean(denied) && denied.includes('denied'), 'a client with the wrong token is refused', 'error frame: ' + JSON.stringify(denied) + ', connection closed: ' + intruder.closed());
45
+ t.check(!intruder.output().includes('LIVE-AGENT'), 'a client with the wrong token sees nothing of the session', tail(intruder.output()));
46
+ t.section('live: counting viewers');
47
+ // Each count is polled: a query that gets no answer yet is asked again, and the number must settle on the expected value.
48
+ const one = await viewersWhen(t, attach, (v) => v.viewers === 1);
49
+ t.check(one !== null, 'a single attached terminal is counted as one viewer', 'last answer: ' + JSON.stringify(await queryViewers(local, 5000)));
50
+ t.equal(one?.remote, 0, 'a viewer on loopback (inside the sandbox) is not counted as remote');
51
+ const second = await openViewer(attach, { cols: 60, rows: 20 });
52
+ viewers.push(second);
53
+ const two = await viewersWhen(t, attach, (v) => v.viewers === 2);
54
+ t.check(two !== null, 'a second attached terminal is counted too', 'last answer: ' + JSON.stringify(await queryViewers(local, 5000)));
55
+ second.close();
56
+ const backToOne = await viewersWhen(t, attach, (v) => v.viewers === 1);
57
+ t.check(backToOne !== null, 'a closed terminal stops counting at once', 'last answer: ' + JSON.stringify(await queryViewers(local, 5000)));
58
+ t.equal(backToOne?.remote, 0, 'the remaining viewer is still not remote');
59
+ t.equal(daemon.status('live1')?.phase, 'running', 'the session keeps running when a terminal closes');
60
+ t.equal(await queryViewers({ ...local, token: 'wrong-token' }, 10000), null, 'the viewer query needs the session token too');
61
+ t.section('live: exec and requests');
62
+ daemon.request({ kind: 'exec', id: 'exec1', command: ['echo', 'hello from exec'], cwd: workspace, timeoutSeconds: 30 });
63
+ const exec = await daemon.response('exec1');
64
+ t.check(Boolean(exec?.ok && exec.stdout?.includes('hello from exec')), 'exec runs a command in the sandbox and returns its output', JSON.stringify(exec));
65
+ daemon.request({ kind: 'exec', id: 'exec2', command: ['dir'], cwd: 'C:\\Windows', timeoutSeconds: 30 });
66
+ const outside = await daemon.response('exec2');
67
+ t.check(outside?.ok === false && outside.message.includes('not inside the sandbox workspace'), 'exec refuses folders outside the workspace', JSON.stringify(outside));
68
+ daemon.request({ kind: 'shell', id: 'evil', command: 'calc' });
69
+ daemon.request({ kind: 'ping', id: 'ping1' });
70
+ const ping = await daemon.response('ping1');
71
+ t.check(Boolean(ping?.ok), 'ping is answered', JSON.stringify(ping));
72
+ // Requests are handled in order, so by the time the ping is answered the unknown one has been dealt with.
73
+ t.check(!fs.existsSync(path.join(daemon.run, 'responses', 'evil.json')), 'an unknown request kind gets no answer...');
74
+ t.check(!fs.existsSync(path.join(daemon.run, 'requests', 'evil.json')), '... and is deleted');
75
+ t.section('live: restart and exit');
76
+ daemon.request({ kind: 'restart', id: 'restart1', harness: 'claude' });
77
+ const sawResumed = await sees(t, viewer, 'LIVE-AGENT-RESUMED');
78
+ t.check(Boolean(sawResumed), 'restart relaunches the agent with its resume arguments while attached', 'screen so far: ' + tail(viewer.output()) + '\n' + daemon.diagnostics());
79
+ viewer.type('exit 3');
80
+ const gotExit = await t.waitFor(() => viewer.exitCode() !== null, WAIT.exit);
81
+ const ended = await t.waitFor(() => { const s = daemon.status('live1'); return s?.phase === 'exited' ? s : null; }, WAIT.exit);
82
+ t.equal(gotExit ? viewer.exitCode() : null, 3, 'the agent\'s exit code reaches the attached client');
83
+ t.equal(ended?.exitCode, 3, 'the agent\'s exit code reaches the session status');
84
+ }
85
+ finally {
86
+ for (const v of viewers)
87
+ v.close();
88
+ await daemon.stop();
89
+ }
90
+ },
91
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const liveGitSuite: Suite;
@@ -0,0 +1,60 @@
1
+ // Live: compiles gitshim.cs with the .NET Framework compiler and runs real git through it in a scratch repository,
2
+ // as git.exe and through Git Bash, in each git access mode.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { findOnPath } from '../core/util.js';
7
+ import { CSC } from './live.js';
8
+ export const liveGitSuite = {
9
+ name: 'live-git',
10
+ title: 'live: the git access guard (compiled git.exe stand-in, Git Bash)',
11
+ live: true,
12
+ run: async (t, f) => {
13
+ const realGit = findOnPath(['git.exe']);
14
+ if (!realGit) {
15
+ t.skip('git guard (no git.exe on PATH)');
16
+ return;
17
+ }
18
+ const gen = await f.launch();
19
+ const shimDir = t.dir('gitshim');
20
+ const compiled = spawnSync(CSC, ['/nologo', '/optimize', '/out:' + path.join(shimDir, 'git.exe'), path.join(gen.launchPath, 'sandbox', 'gitshim.cs')], { encoding: 'utf8' });
21
+ t.check(compiled.status === 0, 'gitshim.cs compiles with the .NET Framework compiler', compiled.stdout + compiled.stderr);
22
+ if (compiled.status !== 0)
23
+ return;
24
+ const repo = t.dir('guard-repo');
25
+ spawnSync(realGit, ['init', '-q'], { cwd: repo });
26
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'hello');
27
+ const guard = path.join(gen.launchPath, 'runtime', 'gitguard.js');
28
+ const env = (access) => ({
29
+ ...process.env,
30
+ PATH: shimDir + path.delimiter + process.env.PATH,
31
+ AGENT_GIT_ACCESS: access,
32
+ AGENT_REAL_GIT: access === 'readonly' ? realGit : '',
33
+ AGENT_GUARD_NODE: process.execPath,
34
+ AGENT_GUARD_SCRIPT: guard,
35
+ GIT_CONFIG_NOSYSTEM: '1',
36
+ });
37
+ // `git` is found through PATH, where the shim comes first (that is the point of the test).
38
+ const git = (access, args) => spawnSync('git', args, { cwd: repo, env: env(access), encoding: 'utf8' });
39
+ t.section('git.exe stand-in');
40
+ const status = git('readonly', ['status', '--porcelain']);
41
+ t.check(status.status === 0 && status.stdout.includes('a.txt'), 'readonly: `git status` works through the git.exe stand-in (exact arguments preserved)', 'exit ' + status.status + ' ' + status.stdout + status.stderr);
42
+ const add = git('readonly', ['add', 'a.txt']);
43
+ t.check(add.status === 128, 'readonly: `git add` is refused with exit code 128', 'exit ' + add.status);
44
+ t.includes(add.stderr, 'read-only', 'readonly: the refusal says why');
45
+ t.includes(git('readonly', ['status', '--porcelain']).stdout, '??', 'readonly: after the refused `git add` nothing has changed');
46
+ const none = git('none', ['status']);
47
+ t.check(none.status === 128 && none.stderr.includes('disabled'), 'none: every git call is refused', 'exit ' + none.status + ' ' + none.stderr);
48
+ t.section('Git Bash');
49
+ const gitRoot = path.dirname(path.dirname(realGit));
50
+ const bash = [path.join(gitRoot, 'bin', 'bash.exe'), path.join(gitRoot, 'usr', 'bin', 'bash.exe')].find((b) => fs.existsSync(b));
51
+ if (!bash) {
52
+ t.skip('Git Bash check (no bash.exe next to git)');
53
+ return;
54
+ }
55
+ const posix = (p) => p.replace(/\\/g, '/');
56
+ const bashEnv = t.write(t.at('gitshim', 'bashenv.sh'), 'git() { "' + posix(process.execPath) + '" "' + posix(guard) + '" "$@"; }\nexport -f git\n');
57
+ const viaBash = spawnSync(bash, ['-c', 'git commit -m x'], { cwd: repo, env: { ...env('readonly'), BASH_ENV: posix(bashEnv) }, encoding: 'utf8' });
58
+ t.check(viaBash.status === 128 && viaBash.stderr.includes('read-only'), 'readonly: git inside Git Bash goes through the guard too (BASH_ENV)', 'exit ' + viaBash.status + ' ' + viaBash.stderr);
59
+ },
60
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const liveProjectsSuite: Suite;