windows-development-sandbox 1.23.2 → 1.25.1

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 (211) hide show
  1. package/config.example.json +5 -3
  2. package/dist/app/features/data/data.def.d.ts +4 -4
  3. package/dist/app/features/data/data.run.d.ts +1 -1
  4. package/dist/app/features/defs.d.ts +161 -6
  5. package/dist/app/features/defs.js +7 -0
  6. package/dist/app/features/projects/projects.def.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.d.ts +1 -1
  8. package/dist/app/features/unreal/index.d.ts +2 -0
  9. package/dist/app/features/unreal/index.js +3 -0
  10. package/dist/app/features/unreal/unreal.def.d.ts +236 -0
  11. package/dist/app/features/unreal/unreal.def.js +379 -0
  12. package/dist/app/features/unreal/unreal.run.d.ts +96 -0
  13. package/dist/app/features/unreal/unreal.run.js +217 -0
  14. package/dist/app/registry.js +5 -0
  15. package/dist/bin/agent-sandbox.js +2 -2
  16. package/dist/bin/{chunk-7YPEDAJD.js → chunk-3GAXLATK.js} +58 -5
  17. package/dist/bin/{chunk-USTRSHIE.js → chunk-F4PVBN23.js} +2615 -1515
  18. package/dist/bin/{chunk-QFSJX2KW.js → chunk-G3TG4ZT7.js} +1 -1
  19. package/dist/bin/claude-host.js +2 -2
  20. package/dist/bin/claude-sbx.js +2 -2
  21. package/dist/bin/codex-sbx.js +2 -2
  22. package/dist/bin/gemini-sbx.js +2 -2
  23. package/dist/bin/sandbox.js +2 -2
  24. package/dist/bin/sbx-hide.js +2 -2
  25. package/dist/bin/sbx-show.js +2 -2
  26. package/dist/cli/commands/launch/launch.js +5 -2
  27. package/dist/cli/commands/registry.js +3 -0
  28. package/dist/cli/manifest.d.ts +18 -0
  29. package/dist/cli/manifest.js +26 -0
  30. package/dist/cli/menu/coverage-exempt.js +1 -0
  31. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  32. package/dist/cli/menu/pages/sandbox-page.js +34 -4
  33. package/dist/cli/menu/views.js +2 -0
  34. package/dist/core/config/defaults.js +18 -5
  35. package/dist/core/detect/toolchains.js +0 -6
  36. package/dist/core/schema/config.d.ts +10 -9
  37. package/dist/core/schema/pc-installs.d.ts +3 -1
  38. package/dist/core/schema/pc-installs.js +10 -1
  39. package/dist/core/schema/toolchains.d.ts +1 -2
  40. package/dist/core/schema/toolchains.js +4 -7
  41. package/dist/guest/accounts/kinds/epic.js +18 -13
  42. package/dist/guest/agents/agent-files.d.ts +11 -7
  43. package/dist/guest/agents/agent-files.js +37 -11
  44. package/dist/guest/agents/local-state.d.ts +2 -1
  45. package/dist/guest/agents/local-state.js +2 -1
  46. package/dist/guest/apps/after-install.d.ts +1 -1
  47. package/dist/guest/apps/after-install.js +6 -1
  48. package/dist/guest/apps/app-installer.d.ts +1 -0
  49. package/dist/guest/apps/app-installer.js +13 -3
  50. package/dist/guest/apps/comet-unpack.js +3 -2
  51. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  52. package/dist/guest/boot/boot-console.d.ts +28 -0
  53. package/dist/guest/boot/boot-console.js +98 -0
  54. package/dist/guest/boot/boot.d.ts +8 -1
  55. package/dist/guest/boot/boot.js +80 -43
  56. package/dist/guest/boot/graph.d.ts +9 -5
  57. package/dist/guest/boot/graph.js +8 -2
  58. package/dist/guest/boot/guide.js +24 -0
  59. package/dist/guest/boot/progress-view.d.ts +116 -0
  60. package/dist/guest/boot/progress-view.js +456 -0
  61. package/dist/guest/boot/runner.d.ts +20 -10
  62. package/dist/guest/boot/runner.js +39 -47
  63. package/dist/guest/daemon/daemon.js +3 -0
  64. package/dist/guest/runtime/log-line.d.ts +7 -0
  65. package/dist/guest/runtime/log-line.js +18 -0
  66. package/dist/guest/runtime/log.d.ts +17 -4
  67. package/dist/guest/runtime/log.js +40 -17
  68. package/dist/guest/runtime/process.d.ts +9 -0
  69. package/dist/guest/runtime/process.js +69 -8
  70. package/dist/guest/setup/addon-mcp.d.ts +7 -0
  71. package/dist/guest/setup/addon-mcp.js +19 -4
  72. package/dist/guest/setup/browser-gpu.js +14 -11
  73. package/dist/guest/setup/environment.js +9 -4
  74. package/dist/guest/setup/headroom.js +4 -0
  75. package/dist/guest/setup/hooks.d.ts +2 -0
  76. package/dist/guest/setup/hooks.js +4 -0
  77. package/dist/guest/setup/path-plan.d.ts +12 -0
  78. package/dist/guest/setup/path-plan.js +18 -0
  79. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  80. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  81. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  82. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  83. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  84. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  85. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  86. package/dist/guest/toolchains/native/debuggers.js +57 -0
  87. package/dist/guest/toolchains/native/mesa.js +15 -8
  88. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  89. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  90. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  91. package/dist/guest/toolchains/native/unreal.js +96 -20
  92. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  93. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  94. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  95. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  96. package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
  97. package/dist/guest/toolchains/native/vs-request.js +16 -50
  98. package/dist/guest/toolchains/plan.js +23 -15
  99. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  100. package/dist/host/accounts/kinds/epic.js +11 -5
  101. package/dist/host/accounts/source.d.ts +2 -0
  102. package/dist/host/accounts/source.js +1 -0
  103. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  104. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  105. package/dist/host/migrate/registry.js +7 -1
  106. package/dist/host/pc-installs/discover.d.ts +2 -0
  107. package/dist/host/pc-installs/discover.js +25 -5
  108. package/dist/host/pc-installs/epic.d.ts +3 -0
  109. package/dist/host/pc-installs/epic.js +33 -2
  110. package/dist/host/pc-installs/source.d.ts +1 -1
  111. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  112. package/dist/host/pc-installs/unreal-vault.js +78 -0
  113. package/dist/host/pc-installs/unreal.d.ts +36 -0
  114. package/dist/host/pc-installs/unreal.js +145 -2
  115. package/dist/host/profile/claude-profile.d.ts +8 -0
  116. package/dist/host/profile/claude-profile.js +43 -4
  117. package/dist/host/profile/index.d.ts +1 -1
  118. package/dist/host/profile/index.js +1 -1
  119. package/dist/runtime/agent.js +148 -33
  120. package/dist/runtime/daemon.js +3146 -1546
  121. package/dist/runtime/db.js +148 -33
  122. package/dist/runtime/main.js +3982 -2049
  123. package/dist/runtime/netproxy.js +84 -13
  124. package/dist/runtime/tool.js +125 -31
  125. package/dist/runtime/view.js +85 -14
  126. package/dist/selftest/agent-paths.test.js +18 -1
  127. package/dist/selftest/agent-settings.test.js +36 -5
  128. package/dist/selftest/agents-web.test.js +12 -0
  129. package/dist/selftest/boot-console.test.d.ts +2 -0
  130. package/dist/selftest/boot-console.test.js +291 -0
  131. package/dist/selftest/boot-runner.test.js +10 -12
  132. package/dist/selftest/engines.test.js +602 -3
  133. package/dist/selftest/guest-boot.test.js +7 -1
  134. package/dist/selftest/heavy-load.test.js +3 -1
  135. package/dist/selftest/hostile-input.test.js +11 -1
  136. package/dist/selftest/index.js +4 -0
  137. package/dist/selftest/layout.test.js +4 -0
  138. package/dist/selftest/menu-screens.js +3 -1
  139. package/dist/selftest/migrate.test.js +21 -0
  140. package/dist/selftest/mixamo.test.d.ts +2 -0
  141. package/dist/selftest/mixamo.test.js +60 -0
  142. package/dist/selftest/persisted-installs.test.js +169 -33
  143. package/dist/selftest/secrets.test.js +36 -1
  144. package/dist/selftest/support/engine-golden.js +5 -0
  145. package/dist/selftest/support/fixtures.d.ts +4 -3
  146. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  147. package/dist/selftest/support/unreal-sample.js +83 -0
  148. package/dist/selftest/toolchains.test.js +32 -2
  149. package/dist/shared/accounts/index.d.ts +1 -1
  150. package/dist/shared/accounts/index.js +1 -1
  151. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  152. package/dist/shared/accounts/kinds/epic.js +11 -1
  153. package/dist/shared/apps/associations.d.ts +7 -0
  154. package/dist/shared/apps/associations.js +26 -0
  155. package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
  156. package/dist/shared/apps/catalogue-agents-web.js +5 -2
  157. package/dist/shared/apps/catalogue-gamedev.js +71 -1
  158. package/dist/shared/apps/index.d.ts +2 -0
  159. package/dist/shared/apps/index.js +2 -0
  160. package/dist/shared/apps/preview.d.ts +2 -0
  161. package/dist/shared/apps/preview.js +10 -0
  162. package/dist/shared/browser-policy.d.ts +14 -0
  163. package/dist/shared/browser-policy.js +22 -0
  164. package/dist/shared/channel/schema.d.ts +3 -6
  165. package/dist/shared/io/index.d.ts +1 -0
  166. package/dist/shared/io/index.js +1 -0
  167. package/dist/shared/io/rotate-log.d.ts +2 -0
  168. package/dist/shared/io/rotate-log.js +16 -0
  169. package/dist/shared/memory/policy.d.ts +5 -0
  170. package/dist/shared/memory/policy.js +7 -0
  171. package/dist/shared/paths/pc-installs.d.ts +13 -2
  172. package/dist/shared/paths/pc-installs.js +1 -0
  173. package/dist/shared/pty/terminal.js +2 -1
  174. package/dist/shared/readiness.d.ts +0 -1
  175. package/dist/shared/readiness.js +0 -1
  176. package/dist/shared/signer.d.ts +4 -0
  177. package/dist/shared/signer.js +2 -0
  178. package/dist/shared/unreal/engines.d.ts +161 -0
  179. package/dist/shared/unreal/engines.js +308 -0
  180. package/dist/shared/unreal/guide.d.ts +19 -0
  181. package/dist/shared/unreal/guide.js +105 -0
  182. package/dist/shared/unreal/index.d.ts +7 -0
  183. package/dist/shared/unreal/index.js +8 -0
  184. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  185. package/dist/shared/unreal/launcher-files.js +292 -0
  186. package/dist/shared/unreal/mixamo.d.ts +5 -0
  187. package/dist/shared/unreal/mixamo.js +16 -0
  188. package/dist/shared/unreal/room.d.ts +17 -0
  189. package/dist/shared/unreal/room.js +34 -0
  190. package/dist/shared/unreal/status.d.ts +54 -0
  191. package/dist/shared/unreal/status.js +57 -0
  192. package/dist/shared/unreal/vault.d.ts +21 -0
  193. package/dist/shared/unreal/vault.js +67 -0
  194. package/dist/shared/unreal/vs-check.d.ts +39 -0
  195. package/dist/shared/unreal/vs-check.js +79 -0
  196. package/dist/sync/watcher.js +5 -1
  197. package/dist/web/index.html +1 -1
  198. package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
  199. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  200. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  201. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  202. package/package.json +1 -1
  203. package/sandbox/csharp/VsSetup.cs +210 -0
  204. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  205. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  206. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  207. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  208. package/schemas/config.schema.json +38 -9
  209. package/schemas/sandbox-config.schema.json +11 -8
  210. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  211. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -8,7 +8,7 @@ import { DEFAULTS } from '../core/config/index.js';
8
8
  import { isWin } from '../core/util/index.js';
9
9
  import { SYSTEM } from '../shared/system.js';
10
10
  import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
11
- import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, } from '../shared/memory/index.js';
11
+ import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, } from '../shared/memory/index.js';
12
12
  import { HeartbeatSchema } from '../shared/channel/index.js';
13
13
  import { heartbeatLine, lastEndOf, vanishedCause, stopWarning } from '../host/life/index.js';
14
14
  import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
@@ -240,6 +240,8 @@ export const heavyLoadSuite = {
240
240
  chooseTargetDir('auto', 10 * 1024 ** 3).targetDir === 'separate' &&
241
241
  Boolean(chooseTargetDir('auto', 10 * 1024 ** 3).warning) &&
242
242
  chooseTargetDir('shared', 1).targetDir === 'shared', 'targetDir "auto": local with 25 GB free on C:, else separate with a warning; other values stay');
243
+ t.check(cargoLinkerVar('x86_64-pc-windows-msvc') === 'CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER' &&
244
+ cargoLinkerVar('aarch64-pc-windows-msvc') === 'CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER', 'cargo linker variable names the host triple (the msvc host links with lld, not link.exe)');
243
245
  const beat = {
244
246
  at: '2026-11-02T21:31:00.000Z',
245
247
  uptimeS: 2800,
@@ -8,7 +8,7 @@ import { assertSafeMapping } from '../core/safe-paths.js';
8
8
  import { safeText } from '../core/util/index.js';
9
9
  import { isSafeFileName } from '../shared/paths/index.js';
10
10
  import { removeRefusal, safeRemove } from '../shared/io/index.js';
11
- import { writeJsonAtomic } from '../shared/io/index.js';
11
+ import { rotateLog, writeJsonAtomic } from '../shared/io/index.js';
12
12
  export const hostileInputSuite = {
13
13
  name: 'hostile-input',
14
14
  title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
@@ -63,5 +63,15 @@ export const hostileInputSuite = {
63
63
  fs.writeFileSync(path.join(repoDir, '.sandbox-config.json'), '{"readOnly": true}' + ' '.repeat(1100 * 1024));
64
64
  await t.throws(() => f.run({ cwd: repoDir, trustConfig: true }), 'over 1 MB', 'a .sandbox-config.json over 1 MB is refused before it is parsed');
65
65
  fs.rmSync(path.join(repoDir, '.sandbox-config.json'));
66
+ t.section('a log that grows for ever (Headroom appends to one on the data disk)');
67
+ const logFile = t.at('rotate', 'proxy.log');
68
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
69
+ t.check(!rotateLog(logFile, 10), 'no log yet: nothing to rotate');
70
+ fs.writeFileSync(logFile, 'x'.repeat(10));
71
+ t.check(!rotateLog(logFile, 10) && fs.existsSync(logFile), 'a log at the limit stays');
72
+ fs.writeFileSync(logFile + '.1', 'old');
73
+ fs.writeFileSync(logFile, 'y'.repeat(11));
74
+ t.check(rotateLog(logFile, 10), 'a log over the limit is rotated');
75
+ t.check(!fs.existsSync(logFile) && fs.readFileSync(logFile + '.1', 'utf8') === 'y'.repeat(11), 'it replaces the older copy');
66
76
  },
67
77
  };
@@ -16,6 +16,7 @@ import { persistedInstallsSuite } from './persisted-installs.test.js';
16
16
  import { pcInstallsSuite } from './pc-installs.test.js';
17
17
  import { steamSuite } from './steam.test.js';
18
18
  import { enginesSuite } from './engines.test.js';
19
+ import { mixamoSuite } from './mixamo.test.js';
19
20
  import { unitySuite } from './unity.test.js';
20
21
  import { fivemSuite } from './fivem.test.js';
21
22
  import { androidSuite } from './android.test.js';
@@ -80,6 +81,7 @@ import { uiTextSuite } from './ui-text.test.js';
80
81
  import { menuCoverageSuite } from './menu-coverage.test.js';
81
82
  import { auditSuite } from './audit.test.js';
82
83
  import { bootRunnerSuite } from './boot-runner.test.js';
84
+ import { bootConsoleSuite } from './boot-console.test.js';
83
85
  import { layoutSuite } from './layout.test.js';
84
86
  import { menuSuite } from './menu.test.js';
85
87
  import { menuPerfSuite } from './menu-perf.test.js';
@@ -133,10 +135,12 @@ export const SUITES = [
133
135
  // pure helpers and the command line
134
136
  layoutSuite,
135
137
  bootRunnerSuite,
138
+ bootConsoleSuite,
136
139
  toolchainsSuite,
137
140
  persistedInstallsSuite,
138
141
  steamSuite,
139
142
  enginesSuite,
143
+ mixamoSuite,
140
144
  unitySuite,
141
145
  fivemSuite,
142
146
  androidSuite,
@@ -19,6 +19,8 @@ const PURE_GUEST = [
19
19
  'guest/boot/project-env.js',
20
20
  'guest/boot/runner.js',
21
21
  'guest/boot/graph.js',
22
+ 'guest/boot/progress-view.js',
23
+ 'guest/runtime/log-line.js',
22
24
  'guest/toolchains/split.js',
23
25
  'guest/toolchains/step-scheduler.js',
24
26
  'guest/audit/parse.js',
@@ -30,6 +32,8 @@ const PURE_GUEST = [
30
32
  'guest/governor/decide.js',
31
33
  'guest/governor/core.js',
32
34
  'guest/daemon/live-folders.js',
35
+ 'guest/toolchains/native/vs-request.js',
36
+ 'guest/toolchains/native/vs-persist.js',
33
37
  ];
34
38
  const SIDE_EFFECTS = ['guest/runtime/session.js', 'guest/runtime/log.js'];
35
39
  const readModules = (dist) => {
@@ -10,6 +10,7 @@ import { appsOptions, stageTicks } from '../cli/menu/pages/apps-page.js';
10
10
  import { maintenanceOptions } from '../cli/menu/pages/maintenance-page.js';
11
11
  import { sandboxOptions } from '../cli/menu/pages/sandbox-page.js';
12
12
  import { sessionsOptions } from '../cli/menu/pages/sessions-page.js';
13
+ import { UNREAL_SAMPLE } from './support/unreal-sample.js';
13
14
  import { settingsOf, settingsOptions } from '../cli/menu/pages/settings-page.js';
14
15
  import { CATEGORIES } from '../app/features/settings/index.js';
15
16
  import { agentsOptions } from '../cli/menu/pages/agents-page.js';
@@ -199,6 +200,7 @@ export function renderScreens(dir) {
199
200
  { name: 'redis', running: false, port: 6379, title: 'Redis' },
200
201
  ],
201
202
  defaultAgent: 'claude',
203
+ unreal: UNREAL_SAMPLE,
202
204
  };
203
205
  const stopped = {
204
206
  ...sandbox,
@@ -399,7 +401,7 @@ export function renderScreens(dir) {
399
401
  out.push(block('1 Agents · Start', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'start' }, size, status), size), block('1 Agents · Options (navigation focused)', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'options' }, size, status, 0, true), size), block('1 Agents · On this PC', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'host' }, size, status), size), block('2 Sessions', draw(1, sessionsOptions({ sandboxRunning: true, sessions: SESSIONS, defaultAgent: 'claude' }), size, status), size), block('2 Sessions (no sandbox)', draw(1, sessionsOptions({ sandboxRunning: false, sessions: [], defaultAgent: 'claude' }), size), size));
400
402
  for (const category of ['status', 'databases'])
401
403
  out.push(block('3 Sandbox (stopped) · ' + category, draw(2, { ...sandboxOptions(stopped), startCategory: category }, size), size));
402
- for (const category of ['status', 'projects', 'databases'])
404
+ for (const category of ['status', 'projects', 'databases', 'unreal'])
403
405
  out.push(block('3 Sandbox · ' + category, draw(2, { ...sandboxOptions(sandbox), startCategory: category }, size, status), size));
404
406
  out.push(block('4 Apps (1 unsaved)', draw(3, appsOptions(user, appsData, appsDraft, 'user'), size, status, appsDraft.count()), size));
405
407
  for (const category of CATEGORIES)
@@ -159,6 +159,27 @@ export const migrateSuite = {
159
159
  finally {
160
160
  fs.rmSync(machineFile);
161
161
  }
162
+ t.section('drop-vs-install-files');
163
+ const dropVs = MIGRATIONS.find((m) => m.id === '2026-11-drop-vs-install-files');
164
+ if (!dropVs)
165
+ return t.check(false, 'the drop-vs-install-files migration exists');
166
+ const oldVs = t.write(t.at('vs', 'old.json'), '// mine\n' +
167
+ JSON.stringify({
168
+ toolchains: { unreal: { enabled: true, keepInstallFiles: true, visualStudio: 'autosdk', components: ['X'] } },
169
+ }));
170
+ const fullVs = t.write(t.at('vs', 'full.json'), JSON.stringify({ toolchains: { unreal: { visualStudio: 'full' } } }));
171
+ const vsNotes = [];
172
+ const vsWanted = dropVs.applies(ctx(t.at('vs'), oldVs)) && !dropVs.applies(ctx(t.at('vs'), fullVs));
173
+ await dropVs.run(ctx(t.at('vs'), oldVs), (line) => vsNotes.push(line));
174
+ const vsAfter = parse(readText(oldVs));
175
+ t.check(vsWanted &&
176
+ vsAfter.toolchains.unreal.keepInstallFiles === undefined &&
177
+ vsAfter.toolchains.unreal.visualStudio === undefined &&
178
+ vsAfter.toolchains.unreal.enabled === true &&
179
+ readText(oldVs).includes('// mine') &&
180
+ vsNotes.join().includes('toolchains.unreal.keepInstallFiles') &&
181
+ loadConfig(oldVs).config.toolchains.unreal.visualStudio === 'full' &&
182
+ !dropVs.applies(ctx(t.at('vs'), oldVs)), 'the retired Visual Studio settings are removed (a chosen "full" stays), the file loads again, and it is idempotent');
162
183
  t.section('sandbox migrate');
163
184
  const cmdUser = t.dir('cmd-user');
164
185
  const cmdConfig = t.write(t.at('cmd-user', 'config.json'), JSON.stringify({ ...JSON.parse(JSON.stringify(old.defaults)), sandbox: { memoryMb: 4096 } }, null, 2));
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const mixamoSuite: Suite;
@@ -0,0 +1,60 @@
1
+ // Mixamo: the browser policy table (downloads folder, no GPU) and the guide's Mixamo lines and editor names.
2
+ import { browserPolicies } from '../shared/browser-policy.js';
3
+ import { unrealGuideLines } from '../shared/unreal/guide.js';
4
+ const DOWNLOADS = 'C:\\AgentData\\downloads';
5
+ const PROJECT = { workspace: 'C:\\P\\Game', uproject: 'C:\\P\\Game\\Game.uproject' };
6
+ function statusWith(version) {
7
+ return {
8
+ updatedAt: 'now',
9
+ engines: [
10
+ {
11
+ id: 'UE_' + version,
12
+ dir: 'C:\\E',
13
+ version,
14
+ build: 'installed',
15
+ origin: 'pc',
16
+ writable: true,
17
+ sizeBytes: null,
18
+ },
19
+ ],
20
+ projects: [{ workspace: PROJECT.workspace, engine: 'C:\\E', reason: 'test' }],
21
+ launcher: { installed: false, installLocation: null, problem: null, freeBytes: null, needBytes: 1 },
22
+ visualStudio: { version: '17.14', path: 'C:/VS', how: 'kept', check: 'ok', ok: true },
23
+ debugger: null,
24
+ ddc: { mode: 'persisted', dir: null },
25
+ vaults: [],
26
+ mcpUrl: null,
27
+ };
28
+ }
29
+ export const mixamoSuite = {
30
+ name: 'mixamo',
31
+ title: 'Mixamo: browser downloads folder and the guide lines for bringing characters into Unreal',
32
+ run: async (t) => {
33
+ t.section('browser policies');
34
+ const four = browserPolicies({ downloads: DOWNLOADS });
35
+ t.equal(four.length, 4, 'two browsers, two policies each');
36
+ t.equal(four.filter((p) => p.name === 'DefaultDownloadDirectory').map((p) => p.data), [DOWNLOADS, DOWNLOADS], 'both browsers download to the data disk');
37
+ t.equal(browserPolicies({ downloads: null }).length, 2, 'no folder: only the GPU policy');
38
+ await t.throws(() => browserPolicies({ downloads: 'C:\\a"b' }), 'not a folder', 'a quote in the folder is refused');
39
+ await t.throws(() => browserPolicies({ downloads: 'downloads' }), 'not a folder', 'a relative folder is refused');
40
+ t.section('the guide');
41
+ const on = { mixamo: { downloads: DOWNLOADS, script: null } };
42
+ const guide = unrealGuideLines(statusWith('5.4.4'), [PROJECT], on).join('\n');
43
+ t.includes(guide, 'https://www.mixamo.com', 'the Mixamo line is there');
44
+ t.includes(guide, DOWNLOADS, 'it names the downloads folder');
45
+ t.includes(guide, 'Mixamo into Unreal', 'and the import line');
46
+ t.excludes(guide, 'Mixamo import script', 'no script line without a script');
47
+ const withScript = unrealGuideLines(statusWith('5.4.4'), [PROJECT], {
48
+ mixamo: { downloads: DOWNLOADS, script: 'C:\\x\\import_mixamo.py' },
49
+ }).join('\n');
50
+ t.includes(withScript, 'Mixamo import script', 'a script adds its line');
51
+ t.excludes(unrealGuideLines(statusWith('5.4.4'), [PROJECT]).join('\n'), 'Mixamo', 'off by default');
52
+ const noEngine = { ...statusWith('5.4.4'), engines: [] };
53
+ t.excludes(unrealGuideLines(noEngine, [PROJECT], on).join('\n'), 'Mixamo', 'no engine: no Mixamo lines');
54
+ t.section('UE4 names');
55
+ const ue4 = unrealGuideLines(statusWith('4.27.2'), [PROJECT]).join('\n');
56
+ t.includes(ue4, 'UE4Editor-Cmd.exe', 'a UE4 engine uses UE4Editor-Cmd.exe');
57
+ t.excludes(ue4, 'UnrealEditor', 'and not the UE5 names');
58
+ t.includes(unrealGuideLines(statusWith('5.4.4'), [PROJECT]).join('\n'), 'UnrealEditor-Cmd.exe', 'UE5 keeps its names');
59
+ },
60
+ };
@@ -1,12 +1,13 @@
1
1
  // Large installs kept between sandboxes (guest/toolchains/persisted.ts): the staging folder and its TEMP, the marker,
2
2
  // the refusal when the disk is short (an injected statfs), keeping only keepVersions, the records `sandbox data`
3
- // reads, and which Visual Studio the toolchains ask for (vs-request.ts). Temp folders only.
3
+ // reads, which Visual Studio the toolchains ask for (vs-request.ts), and how it is saved on the data disk and put
4
+ // back (vs-persist.ts: folders, marker, instance state, registry). Temp folders only.
4
5
  import fs from 'node:fs';
5
6
  import path from 'node:path';
6
7
  import { DEFAULTS } from '../core/config/index.js';
7
8
  import { assertLocalRoom, ensurePersisted, installRecords, InstallRecordsSchema, LOCAL_RESERVE_BYTES, recordUse, } from '../guest/toolchains/persisted.js';
8
- import { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, } from '../guest/toolchains/native/vs-request.js';
9
- import { autosdkLayout, layoutNextStep, STAGING_RESERVE_BYTES, unrealUsesAutosdk, vsExitText, } from '../guest/toolchains/native/vs-request.js';
9
+ import { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, vsExitText, } from '../guest/toolchains/native/vs-request.js';
10
+ import { markerFrom, newestInstance, persistCovers, rebase, registryPlan, restoreInput, rewriteInstanceState, SaveResultSchema, saveInput, SETUP_CLSID, VsPersistMarkerSchema, } from '../guest/toolchains/native/vs-persist.js';
10
11
  import { freeBytesAt } from '../shared/io/disk.js';
11
12
  import { SIGNERS, signerMatches } from '../shared/signer.js';
12
13
  import { decideToolchains } from '../core/detect/toolchains.js';
@@ -17,7 +18,7 @@ import { collectDataEntries } from '../cli/services/data-collect.js';
17
18
  const GB = 1024 ** 3;
18
19
  export const persistedInstallsSuite = {
19
20
  name: 'persisted-installs',
20
- title: 'large persisted installs: staging, marker, room checks, pruning, records; the Visual Studio request',
21
+ title: 'large persisted installs: staging, marker, room checks, pruning, records; Visual Studio: request, save, restore',
21
22
  run: async (t) => {
22
23
  t.section('installing a version');
23
24
  const tools = t.dir('persisted-tools');
@@ -110,28 +111,172 @@ export const persistedInstallsSuite = {
110
111
  all.for.join() === 'unreal,flutter,unity' &&
111
112
  UNREAL_COMPONENTS.every((c) => all.components.includes(c)) &&
112
113
  all.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), 'with Unreal: one Community install serves all of them');
113
- t.section('Unreal on the portable MSVC (autosdk)');
114
- tc.unreal.visualStudio = 'autosdk';
115
- const noUnreal = visualStudioWanted(tc);
116
- t.check(noUnreal?.product === 'buildtools' && noUnreal.for.join() === 'flutter,unity', 'autosdk: Unreal asks for no Visual Studio (the others still get the Build Tools)');
117
- tc.flutter.enabled = false;
118
- tc.unity.enabled = false;
119
- t.equal(visualStudioWanted(tc), null, 'autosdk alone: no Visual Studio at all');
120
- t.check(unrealUsesAutosdk(tc, (s) => s === true) && !unrealUsesAutosdk(DEFAULTS.toolchains), 'autosdk is only when asked');
121
- const pairs = autosdkLayout('T:\\msvc', '14.44.35207', '10.0.26100.0', 'T:\\autosdk');
122
- t.check(pairs.length === 4 &&
123
- pairs[0].to === 'T:\\autosdk\\HostWin64\\Win64\\VS2022\\14.44.35207' &&
124
- pairs[0].from === 'T:\\msvc\\VC\\Tools\\MSVC\\14.44.35207' &&
125
- pairs[1].from === 'T:\\msvc\\Windows Kits\\10\\Include\\10.0.26100.0' &&
126
- pairs[1].to === 'T:\\autosdk\\HostWin64\\Win64\\Windows Kits\\10\\Include\\10.0.26100.0', 'the AutoSDK tree is MSVC under VS2022\\<version> and the SDK under Windows Kits\\10 (copies)');
114
+ t.section('Unreal on the Build Tools');
115
+ tc.unreal.visualStudio = 'buildtools';
116
+ const bt = visualStudioWanted(tc);
117
+ t.check(bt?.product === 'buildtools' &&
118
+ bt.for.join() === 'unreal,flutter,unity' &&
119
+ bt.licence === 'toolchains.unreal.acceptLicense' &&
120
+ UNREAL_BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
121
+ BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
122
+ bt.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), '"buildtools": the Build Tools with what UnrealBuildTool needs, shared with Flutter and Unity');
123
+ t.equal(DEFAULTS.toolchains.unreal.visualStudio, 'full', 'Community is the default (the IDE, its debugger and Unreal integration)');
127
124
  const on = DEFAULTS.toolchains;
128
- const auto = {
129
- ...structuredClone(on),
130
- unreal: { ...on.unreal, visualStudio: 'autosdk', acceptLicense: true },
125
+ const decided = decideToolchains({ ...structuredClone(on), unreal: { ...on.unreal, acceptLicense: true } }, ['Game.uproject'], []);
126
+ t.check(decided.unreal.on && !decided.msvc.on, 'an Unreal project does not turn the portable MSVC on (Visual Studio brings MSVC)');
127
+ t.section('Visual Studio kept on the data disk');
128
+ const places = {
129
+ programFiles: 'C:\\Program Files',
130
+ programFilesX86: 'C:\\Program Files (x86)',
131
+ programData: 'C:\\ProgramData',
131
132
  };
132
- const decided = decideToolchains(auto, ['Game.uproject'], []);
133
- t.check(decided.unreal.on && decided.msvc.on, 'an Unreal project with autosdk turns the portable MSVC on, under its licence');
134
- t.section('the Visual Studio layout');
133
+ const root = 'C:\\AgentData\\tools\\visualstudio\\persist';
134
+ const community = 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community';
135
+ const instances = 'C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Instances';
136
+ const vsAt = { product: 'community', installationPath: community, instanceId: 'a190d188' };
137
+ const save = saveInput(vsAt, root, places, 'T:\\result.json');
138
+ const saved = (name) => save.copies.find((c) => c.name === name);
139
+ t.check(saved('Community')?.from === community &&
140
+ saved('Community')?.to === root + '\\Community' &&
141
+ saved('WindowsKits10')?.from === 'C:\\Program Files (x86)\\Windows Kits\\10' &&
142
+ saved('instance')?.from === instances + '\\a190d188' &&
143
+ saved('instance')?.to === root + '\\instance' &&
144
+ saved('Setup')?.from === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup' &&
145
+ saved('Installer')?.optional === false &&
146
+ saved('ReferenceAssemblies')?.optional === true &&
147
+ save.resultFile === 'T:\\result.json', 'the capture mirrors the install, the Windows SDK, the installer, the Setup API and the instance onto the data disk');
148
+ t.check(save.registry.length === 8 &&
149
+ save.registry.filter((r) => r.view === '32').length === 4 &&
150
+ save.registry.some((r) => r.key.endsWith('NETFXSDK') && r.recurse) &&
151
+ save.registry.some((r) => r.key.endsWith('Installed Roots') && r.names?.join() === 'KitsRoot10'), 'it reads the SDK registry values in both views (NETFXSDK with its subkeys, only KitsRoot10 of the kits roots)');
152
+ await t.throws(() => Promise.resolve(saveInput({ ...vsAt, instanceId: '..\\x' }, root, places, 'r')), 'not a Visual Studio instance ID', 'an instance ID is never a path');
153
+ const captured = [
154
+ {
155
+ view: '64',
156
+ key: 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots',
157
+ name: 'KitsRoot10',
158
+ kind: 'string',
159
+ value: 'C:\\Program Files (x86)\\Windows Kits\\10\\',
160
+ },
161
+ {
162
+ view: '32',
163
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0',
164
+ name: 'ProductVersion',
165
+ kind: 'string',
166
+ value: '10.0.22621',
167
+ },
168
+ {
169
+ view: '32',
170
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8',
171
+ name: 'KitsInstallationFolder',
172
+ kind: 'string',
173
+ value: 'C:\\Program Files (x86)\\Windows Kits\\NETFXSDK\\4.8\\',
174
+ },
175
+ {
176
+ view: '32',
177
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8\\WinSDK-NetFx40Tools-x64',
178
+ name: 'InstallationFolder',
179
+ kind: 'string',
180
+ value: 'c:\\program files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x64\\',
181
+ },
182
+ {
183
+ view: '64',
184
+ key: 'SOFTWARE\\Microsoft\\VisualStudio\\Setup',
185
+ name: 'KeepDownloadedPayloads',
186
+ kind: 'dword',
187
+ value: 0,
188
+ },
189
+ {
190
+ view: '64',
191
+ key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
192
+ name: 'planted',
193
+ kind: 'string',
194
+ value: 'C:\\planted.exe',
195
+ },
196
+ ];
197
+ const result = SaveResultSchema.parse({
198
+ copied: [
199
+ 'Community',
200
+ 'WindowsKits10',
201
+ 'NETFXSDK',
202
+ 'MicrosoftSDKs-v10.0A',
203
+ 'ReferenceAssemblies',
204
+ 'Installer',
205
+ ].concat(['Setup', 'instance']),
206
+ registry: captured,
207
+ });
208
+ const marker = VsPersistMarkerSchema.parse(markerFrom({ product: 'community', components: [...UNREAL_COMPONENTS] }, { installationPath: community, instanceId: 'a190d188', version: '17.14.37827.9' }, result, '2026-11-04T00:00:00.000Z'));
209
+ t.check(marker.sdkVersion === '10.0.22621' && marker.instanceId === 'a190d188' && marker.folders.length === 8, 'the marker keeps the instance ID, what was saved and the Windows SDK version from the registry');
210
+ t.check(!VsPersistMarkerSchema.safeParse({ ...marker, instanceId: '..\\..\\Windows' }).success &&
211
+ !VsPersistMarkerSchema.safeParse({ ...marker, folders: ['..'] }).success, 'a marker with a path for its instance ID, or a folder it does not know, is refused');
212
+ const plan = registryPlan(marker, root, places);
213
+ const value = (view, key, name) => plan.find((e) => e.view === view && e.key.toLowerCase() === key.toLowerCase() && e.name === name)?.value;
214
+ const kits = root + '\\WindowsKits10\\';
215
+ const sdk = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0';
216
+ const netfx = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8';
217
+ t.check(['64', '32'].every((v) => value(v, 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', 'KitsRoot10') === kits &&
218
+ value(v, sdk, 'InstallationFolder') === kits &&
219
+ value(v, sdk, 'ProductVersion') === '10.0.22621' &&
220
+ value(v, netfx, 'KitsInstallationFolder') === root + '\\NETFXSDK\\4.8\\' &&
221
+ value(v, netfx + '\\WinSDK-NetFx40Tools-x64', 'InstallationFolder') ===
222
+ root + '\\MicrosoftSDKs-v10.0A\\bin\\NETFX 4.8 Tools\\x64\\' &&
223
+ value(v, 'SOFTWARE\\Microsoft\\VisualStudio\\Setup', 'KeepDownloadedPayloads') === 0), 'both registry views point the Windows SDK and the .NET Framework SDK at the data disk (any case); the rest stays');
224
+ const clsid = 'SOFTWARE\\Classes\\CLSID\\' + SETUP_CLSID + '\\InprocServer32';
225
+ const setupDll = 'Microsoft.VisualStudio.Setup.Configuration.Native.dll';
226
+ t.check(value('64', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x64\\' + setupDll &&
227
+ value('32', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x86\\' + setupDll &&
228
+ value('64', clsid, 'ThreadingModel') === 'Both' &&
229
+ value('32', clsid, 'ThreadingModel') === 'Both', 'the Setup Configuration COM class is registered (x64 and x86), so vswhere and UnrealBuildTool find the instance');
230
+ t.check(!plan.some((e) => /CurrentVersion\\Run/i.test(e.key)), 'a value the capture never reads is never written back (a tampered marker)');
231
+ const back = restoreInput(marker, root, places);
232
+ const copied = (name) => back.copies.find((c) => c.name === name);
233
+ t.check(!copied('Community') &&
234
+ !copied('WindowsKits10') &&
235
+ !copied('NETFXSDK') &&
236
+ !copied('Shared') &&
237
+ copied('Installer')?.to === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer' &&
238
+ copied('Installer')?.mode === 'all' &&
239
+ copied('ReferenceAssemblies')?.mode === 'missing' &&
240
+ copied('instance')?.from === root + '\\instance' &&
241
+ copied('instance')?.to === instances + '\\a190d188' &&
242
+ back.registry.length === plan.length, 'a boot copies back only the small fixed-path folders that were saved (the instance under its ID); the big ones stay');
243
+ t.check(persistCovers(marker, { product: 'community', components: [...UNREAL_COMPONENTS] }) &&
244
+ persistCovers(marker, { product: 'community', components: ['component.unreal'] }) &&
245
+ !persistCovers(marker, {
246
+ product: 'community',
247
+ components: [...UNREAL_COMPONENTS, 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'],
248
+ }) &&
249
+ persistCovers(marker, { product: 'buildtools', components: [...BUILD_TOOLS_COMPONENTS] }) &&
250
+ !persistCovers({ ...marker, product: 'buildtools' }, { product: 'community', components: [] }) &&
251
+ !persistCovers({ ...marker, folders: marker.folders.filter((f) => f !== 'Setup') }, {
252
+ product: 'community',
253
+ components: [],
254
+ }), 'a saved copy serves requests with no more components (Community serves the Build Tools too); one without the Setup API none');
255
+ const state = JSON.stringify({
256
+ installationPath: community,
257
+ resolvedInstallationPath: community.toLowerCase(),
258
+ nested: community + '\\Common7\\IDE',
259
+ longer: community + '2',
260
+ launchParams: { fileName: 'Common7\\IDE\\devenv.exe' },
261
+ enginePath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app',
262
+ });
263
+ const moved = JSON.parse(rewriteInstanceState(state, community, root + '\\Community'));
264
+ t.check(moved.installationPath === root + '\\Community' &&
265
+ moved.resolvedInstallationPath === root + '\\Community' &&
266
+ moved.nested === root + '\\Community\\Common7\\IDE' &&
267
+ moved.longer === community + '2' &&
268
+ moved.enginePath === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app', "the instance's state names the saved folder (JSON-escaped, any case); a longer name that only starts the same stays");
269
+ const buildTools = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools';
270
+ const movedBt = JSON.parse(rewriteInstanceState(JSON.stringify({ installationPath: buildTools }), buildTools + '\\', root + '\\BuildTools'));
271
+ t.check(movedBt.installationPath === root + '\\BuildTools' &&
272
+ rebase('D:\\Other\\x', [{ from: 'C:\\Program Files', to: 'D:\\y' }]) === 'D:\\Other\\x', 'the Build Tools too (a trailing backslash does not matter); a value outside the saved folders is kept');
273
+ t.equal(newestInstance([
274
+ { installationVersion: '17.9.1', id: 'a' },
275
+ { installationVersion: '17.14.37827.9', id: 'b' },
276
+ { installationVersion: '17.14.3', id: 'c' },
277
+ ])?.id, 'b', 'the newest instance is chosen by version (vswhere -latest fails on two instances installed at the same time)');
278
+ t.equal(newestInstance([]), null, 'no instance: none');
279
+ t.section('the Visual Studio installer');
135
280
  t.check(vsExitText(3762504530).includes('crashed') &&
136
281
  vsExitText(-1).includes('exit 4294967295') &&
137
282
  vsExitText(0) === 'installed' &&
@@ -139,15 +284,6 @@ export const persistedInstallsSuite = {
139
284
  vsExitText(8006).includes('running') &&
140
285
  vsExitText(3010).includes('restart') &&
141
286
  vsExitText(5007).includes('refused'), 'installer exit codes are told in words (0xE0434352 is a crash)');
142
- const reserve = LOCAL_RESERVE_BYTES;
143
- const room = (free) => freeBytesAt(t.dir('layout-room'), () => ({ bavail: free, bsize: 1 })) ?? 0;
144
- t.check(layoutNextStep(0, 0, reserve) === 'done' &&
145
- layoutNextStep(3762504530, room(40 * GB), reserve) === 'stage' &&
146
- layoutNextStep(3762504530, room(20 * GB), reserve) === 'skip' &&
147
- layoutNextStep(null, room(40 * GB), reserve) === 'stage', 'a failed layout is built on the sandbox disk only with room for it (8 GB plus the reserve)');
148
- t.check(layoutNextStep(null, room(14 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
149
- layoutNextStep(null, room(9.5 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
150
- layoutNextStep(null, room(9 * GB), STAGING_RESERVE_BYTES) === 'skip', 'before Visual Studio is installed, the ~14 GB free at boot is enough to build the install files (9.5 GB needed)');
151
287
  t.equal(freeBytesAt(path.join(t.dir('layout-room'), 'not', 'there'), () => ({ bavail: 3, bsize: 2 })), 6, 'free room of a folder that does not exist asks its parent');
152
288
  t.equal(freeBytesAt(t.dir('layout-room'), () => {
153
289
  throw new Error('no');
@@ -1,7 +1,9 @@
1
1
  // What reaches the sandbox from this PC's Claude files, and what is stripped first: API keys, cloud credentials, tokens
2
2
  // in MCP server definitions. Also the secret-looking files in a workspace that the launch warns about.
3
3
  import path from 'node:path';
4
- import { findSecretFiles, sandboxGlobalState, stripMcpServers, stripSettings } from '../host/profile/index.js';
4
+ import fs from 'node:fs';
5
+ import { DEFAULTS } from '../core/config/index.js';
6
+ import { findSecretFiles, pluginFilter, pluginTrimLevel, sandboxGlobalState, stageClaudeProfile, stripMcpServers, stripSettings, } from '../host/profile/index.js';
5
7
  const REMOVED = '[removed by agent-sandbox]';
6
8
  export const secretsSuite = {
7
9
  name: 'secrets',
@@ -111,5 +113,38 @@ export const secretsSuite = {
111
113
  ])
112
114
  t.write(path.join(workspace, file), 'x');
113
115
  t.equal(findSecretFiles(workspace).sort(), ['.env', 'api\\credentials.json', 'server.pem'], 'secret-looking workspace files are found for the launch warning (templates and dependencies ignored)');
116
+ t.section('plugins staged from this PC');
117
+ const hostClaude = t.at('plugins-host');
118
+ const plugins = path.join(hostClaude, 'plugins');
119
+ t.write(path.join(plugins, 'installed_plugins.json'), '{"version":2,"plugins":{}}');
120
+ t.write(path.join(plugins, 'known_marketplaces.json'), '{}');
121
+ t.write(path.join(plugins, 'cache', 'm', 'p', '1.0', 'plugin.json'), '{}');
122
+ t.write(path.join(plugins, 'cache', 'm', 'p', '1.0', 'node_modules', 'dep', 'index.js'), 'x');
123
+ t.write(path.join(plugins, 'cache', 'temp_git_1_abc', 'junk.txt'), 'x');
124
+ t.write(path.join(plugins, 'marketplaces', 'm', 'marketplace.json'), '{}');
125
+ t.write(path.join(plugins, 'marketplaces', 'm', '.git', 'HEAD'), 'ref');
126
+ t.write(path.join(plugins, 'marketplaces', 'm.backup', 'old.txt'), 'x');
127
+ t.write(path.join(plugins, 'marketplaces', 'temp_subdir_2.clone', 'junk.txt'), 'x');
128
+ t.write(path.join(hostClaude, 'settings.json'), '{"enabledPlugins":{"p@m":true}}');
129
+ const launch = t.at('plugins-launch');
130
+ const profile = { ...DEFAULTS.claudeProfile, hostDir: hostClaude, globalStateFile: '' };
131
+ const staged = stageClaudeProfile(profile, launch, 'D:/Code/app', DEFAULTS.secrets);
132
+ const out = path.join(launch, 'claude-profile', 'plugins');
133
+ t.check(staged.staged.includes('plugins'), 'plugins are staged');
134
+ t.check(fs.existsSync(path.join(out, 'cache', 'm', 'p', '1.0', 'plugin.json')) &&
135
+ fs.existsSync(path.join(out, 'marketplaces', 'm', 'marketplace.json')) &&
136
+ fs.existsSync(path.join(out, 'marketplaces', 'm', '.git', 'HEAD')) &&
137
+ fs.existsSync(path.join(out, 'cache', 'm', 'p', '1.0', 'node_modules', 'dep', 'index.js')), 'the plugin cache, marketplaces and (when they fit) their git folders and dependencies are copied');
138
+ t.check(!fs.existsSync(path.join(out, 'cache', 'temp_git_1_abc')) &&
139
+ !fs.existsSync(path.join(out, 'marketplaces', 'm.backup')) &&
140
+ !fs.existsSync(path.join(out, 'marketplaces', 'temp_subdir_2.clone')), 'interrupted clones, backups and staging folders of plugins are left out');
141
+ t.check(pluginTrimLevel(plugins) === 0, 'a plugins folder under the cap is not trimmed');
142
+ const git = path.join(plugins, 'marketplaces', 'm', '.git');
143
+ const deps = path.join(plugins, 'cache', 'm', 'p', '1.0', 'node_modules');
144
+ t.check(pluginFilter(plugins, 0)(git) &&
145
+ !pluginFilter(plugins, 1)(git) &&
146
+ pluginFilter(plugins, 1)(deps) &&
147
+ !pluginFilter(plugins, 2)(deps), 'a plugins folder over the cap loses git history first, then node_modules');
148
+ t.check(fs.readFileSync(path.join(launch, 'claude-profile', 'settings.json'), 'utf8').includes('p@m'), 'enabledPlugins reach the sandbox');
114
149
  },
115
150
  };
@@ -69,6 +69,7 @@ export const LAUNCH_FILES_GOLDEN = [
69
69
  'sandbox/csharp/SandboxTools/MemoryLog.cs',
70
70
  'sandbox/csharp/SandboxTools/PageFile.cs',
71
71
  'sandbox/csharp/SandboxTools/Program.cs',
72
+ 'sandbox/csharp/VsSetup.cs',
72
73
  'sandbox/csharp/VsWhere.cs',
73
74
  'sandbox/csharp/VtInput.cs',
74
75
  'sandbox/scripts/add-firewall-port.ps1',
@@ -76,13 +77,17 @@ export const LAUNCH_FILES_GOLDEN = [
76
77
  'sandbox/scripts/expand-archive.ps1',
77
78
  'sandbox/scripts/get-gpu-names.ps1',
78
79
  'sandbox/scripts/get-signature.ps1',
80
+ 'sandbox/scripts/get-vs-setup.ps1',
79
81
  'sandbox/scripts/install-shell-path.ps1',
80
82
  'sandbox/scripts/link-profile-dirs.ps1',
81
83
  'sandbox/scripts/new-shortcuts.ps1',
82
84
  'sandbox/scripts/repair-wmi-services.ps1',
85
+ 'sandbox/scripts/restore-visual-studio.ps1',
86
+ 'sandbox/scripts/save-visual-studio.ps1',
83
87
  'sandbox/scripts/set-desktop.ps1',
84
88
  'sandbox/scripts/set-dns-servers.ps1',
85
89
  'sandbox/scripts/set-machine-environment.ps1',
90
+ 'sandbox/scripts/set-registry-values.ps1',
86
91
  'sandbox/templates/fivem/README.txt',
87
92
  'sandbox/templates/fivem/cs/Client/Client.csproj',
88
93
  'sandbox/templates/fivem/cs/Client/ClientMain.cs',
@@ -142,7 +142,9 @@ export declare function createFixtures(t: Harness): {
142
142
  pcInstalls: {
143
143
  steamGames: boolean | number[];
144
144
  epicGames: boolean | string[];
145
- unrealEngines: boolean | string[];
145
+ unrealEngines: boolean | string[] | "auto";
146
+ unrealEngineFolders: string[];
147
+ unrealAssets: boolean | "auto";
146
148
  unityEditors: boolean | string[];
147
149
  };
148
150
  provisioning: {
@@ -199,8 +201,7 @@ export declare function createFixtures(t: Harness): {
199
201
  enabled: boolean | "auto";
200
202
  acceptLicense: boolean;
201
203
  components: string[];
202
- keepInstallFiles: boolean;
203
- visualStudio: "full" | "autosdk";
204
+ visualStudio: "full" | "buildtools";
204
205
  persistEngines: boolean;
205
206
  derivedDataCache: "persisted" | "local";
206
207
  };
@@ -0,0 +1,2 @@
1
+ import type { UnrealOverview } from '../../app/features/unreal/index.js';
2
+ export declare const UNREAL_SAMPLE: UnrealOverview;