windows-development-sandbox 1.18.3 → 1.20.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 (216) hide show
  1. package/README.md +1 -1
  2. package/config.example.json +9 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +4 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  5. package/dist/app/features/accounts/services.def.d.ts +3 -0
  6. package/dist/app/features/accounts/services.def.js +4 -2
  7. package/dist/app/features/accounts/services.run.d.ts +1 -0
  8. package/dist/app/features/accounts/services.run.js +3 -1
  9. package/dist/app/features/agent-commands/agent-commands.def.d.ts +37 -0
  10. package/dist/app/features/agent-commands/agent-commands.def.js +91 -0
  11. package/dist/app/features/agent-commands/agent-commands.run.d.ts +28 -0
  12. package/dist/app/features/agent-commands/agent-commands.run.js +35 -0
  13. package/dist/app/features/agent-commands/index.d.ts +2 -0
  14. package/dist/app/features/agent-commands/index.js +3 -0
  15. package/dist/app/features/agents/agents.def.d.ts +14 -1
  16. package/dist/app/features/agents/agents.def.js +61 -6
  17. package/dist/app/features/agents/agents.run.d.ts +1 -0
  18. package/dist/app/features/agents/agents.run.js +8 -1
  19. package/dist/app/features/agents/claude-env.def.d.ts +36 -0
  20. package/dist/app/features/agents/claude-env.def.js +305 -0
  21. package/dist/app/features/agents/env.def.d.ts +163 -0
  22. package/dist/app/features/agents/env.def.js +184 -0
  23. package/dist/app/features/agents/env.run.d.ts +43 -0
  24. package/dist/app/features/agents/env.run.js +86 -0
  25. package/dist/app/features/agents/index.d.ts +2 -0
  26. package/dist/app/features/agents/index.js +2 -0
  27. package/dist/app/features/defs.d.ts +150 -1
  28. package/dist/app/features/defs.js +11 -1
  29. package/dist/app/features/sandbox/sandbox.def.d.ts +22 -0
  30. package/dist/app/features/sandbox/sandbox.def.js +41 -14
  31. package/dist/app/features/sandbox/sandbox.run.d.ts +20 -0
  32. package/dist/app/features/sandbox/sandbox.run.js +41 -1
  33. package/dist/app/features/settings/categories.def.js +5 -2
  34. package/dist/app/features/settings/settings-rows.js +5 -1
  35. package/dist/app/features/settings/settings.run.d.ts +8 -0
  36. package/dist/app/features/settings/settings.run.js +9 -0
  37. package/dist/app/features/updates/updates.run.js +12 -2
  38. package/dist/app/features/vpn/vpn.def.d.ts +33 -0
  39. package/dist/app/features/vpn/vpn.def.js +30 -6
  40. package/dist/app/features/vpn/vpn.run.d.ts +11 -0
  41. package/dist/app/features/vpn/vpn.run.js +4 -1
  42. package/dist/app/kernel/rpc.d.ts +1 -1
  43. package/dist/app/registry.js +9 -2
  44. package/dist/bin/agent-sandbox.js +2 -2
  45. package/dist/bin/{chunk-M6XUQK53.js → chunk-2IMT7ZCT.js} +1 -1
  46. package/dist/bin/{chunk-3G53NNZP.js → chunk-DK4NTBUF.js} +69 -26
  47. package/dist/bin/{chunk-FQWYCF6L.js → chunk-Z2P5OGKL.js} +6427 -5259
  48. package/dist/bin/claude-host.js +2 -2
  49. package/dist/bin/claude-sbx.js +2 -2
  50. package/dist/bin/codex-sbx.js +2 -2
  51. package/dist/bin/gemini-sbx.js +2 -2
  52. package/dist/bin/sandbox.js +2 -2
  53. package/dist/bin/sbx-hide.js +2 -2
  54. package/dist/bin/sbx-show.js +2 -2
  55. package/dist/cli/commands/agent-commands.d.ts +0 -1
  56. package/dist/cli/commands/agent-commands.js +2 -32
  57. package/dist/cli/commands/index.d.ts +1 -1
  58. package/dist/cli/commands/index.js +1 -1
  59. package/dist/cli/commands/launch/launch.js +6 -3
  60. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  61. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  62. package/dist/cli/commands/registry.js +3 -2
  63. package/dist/cli/commands/repair/checks/sandbox.js +29 -2
  64. package/dist/cli/manifest.d.ts +1 -1
  65. package/dist/cli/manifest.js +1 -1
  66. package/dist/cli/menu/menu.d.ts +1 -1
  67. package/dist/cli/menu/menu.js +3 -3
  68. package/dist/cli/menu/pages/agents-page.d.ts +7 -2
  69. package/dist/cli/menu/pages/agents-page.js +67 -11
  70. package/dist/cli/menu/pages/maintenance-page.js +1 -2
  71. package/dist/cli/menu/pages/settings-page.js +1 -0
  72. package/dist/cli/menu/views.js +2 -1
  73. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  74. package/dist/cli/services/agent-commands-service.js +13 -7
  75. package/dist/cli/services/vpn-service.d.ts +5 -1
  76. package/dist/cli/services/vpn-service.js +6 -1
  77. package/dist/cli/tui/dialogs/text-screen.js +2 -1
  78. package/dist/cli/tui/terminal.js +3 -1
  79. package/dist/core/config/defaults.js +9 -0
  80. package/dist/core/detect/toolchains.js +9 -0
  81. package/dist/core/disk.d.ts +1 -5
  82. package/dist/core/disk.js +1 -16
  83. package/dist/core/schema/codex.d.ts +29 -0
  84. package/dist/core/schema/codex.js +24 -0
  85. package/dist/core/schema/config.d.ts +46 -14
  86. package/dist/core/schema/config.js +2 -0
  87. package/dist/core/schema/index.d.ts +1 -0
  88. package/dist/core/schema/index.js +1 -0
  89. package/dist/core/schema/toolchains.d.ts +4 -0
  90. package/dist/core/schema/toolchains.js +3 -0
  91. package/dist/core/types.d.ts +2 -0
  92. package/dist/guest/accounts/apply.d.ts +6 -6
  93. package/dist/guest/agents/agent-files.d.ts +26 -6
  94. package/dist/guest/agents/agent-files.js +84 -14
  95. package/dist/guest/agents/local-state.d.ts +5 -2
  96. package/dist/guest/agents/local-state.js +24 -10
  97. package/dist/guest/boot/boot.js +10 -5
  98. package/dist/guest/daemon/heartbeat.d.ts +7 -2
  99. package/dist/guest/daemon/heartbeat.js +34 -15
  100. package/dist/guest/daemon/idle-shutdown.js +2 -0
  101. package/dist/guest/daemon/sessions.js +2 -1
  102. package/dist/guest/entries/daemon.js +1 -1
  103. package/dist/guest/toolchains/installers.d.ts +6 -10
  104. package/dist/guest/toolchains/installers.js +4 -12
  105. package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
  106. package/dist/guest/toolchains/native/autosdk.js +50 -0
  107. package/dist/guest/toolchains/native/unreal.js +3 -0
  108. package/dist/guest/toolchains/native/vcruntime.d.ts +0 -3
  109. package/dist/guest/toolchains/native/vcruntime.js +2 -5
  110. package/dist/guest/toolchains/native/visual-studio.d.ts +2 -2
  111. package/dist/guest/toolchains/native/visual-studio.js +100 -16
  112. package/dist/guest/toolchains/native/vs-request.d.ts +21 -0
  113. package/dist/guest/toolchains/native/vs-request.js +60 -1
  114. package/dist/guest/toolchains/not-yet.d.ts +1 -1
  115. package/dist/guest/toolchains/not-yet.js +3 -3
  116. package/dist/guest/toolchains/persisted.js +2 -7
  117. package/dist/guest/toolchains/plan.js +9 -4
  118. package/dist/guest/toolchains/signature.d.ts +5 -0
  119. package/dist/guest/toolchains/signature.js +22 -0
  120. package/dist/guest/vpn/tunnel.d.ts +3 -0
  121. package/dist/guest/vpn/tunnel.js +10 -12
  122. package/dist/guest/vpn/vpn.js +6 -1
  123. package/dist/host/accounts/leases.d.ts +3 -3
  124. package/dist/host/accounts/source.js +1 -1
  125. package/dist/host/agent-commands/commands.js +1 -1
  126. package/dist/host/agent-commands/forwarders.d.ts +14 -2
  127. package/dist/host/agent-commands/forwarders.js +31 -13
  128. package/dist/host/agent-commands/reconcile.d.ts +2 -1
  129. package/dist/host/agent-commands/reconcile.js +7 -8
  130. package/dist/host/agent-commands/user-path.d.ts +5 -0
  131. package/dist/host/agent-commands/user-path.js +13 -0
  132. package/dist/host/health/index.d.ts +1 -0
  133. package/dist/host/health/index.js +1 -0
  134. package/dist/host/health/leftovers.d.ts +3 -0
  135. package/dist/host/health/leftovers.js +40 -0
  136. package/dist/host/health/stale.d.ts +48 -0
  137. package/dist/host/health/stale.js +40 -0
  138. package/dist/host/launch/session-file.js +1 -0
  139. package/dist/host/life/crash-report.d.ts +1 -1
  140. package/dist/host/life/crash-report.js +5 -3
  141. package/dist/host/life/last-end.d.ts +7 -0
  142. package/dist/host/life/last-end.js +21 -0
  143. package/dist/host/life/life-record.d.ts +3 -0
  144. package/dist/host/life/life-record.js +2 -2
  145. package/dist/host/life/stop-events.d.ts +3 -1
  146. package/dist/host/life/stop-events.js +59 -0
  147. package/dist/host/life/stop-warning.js +3 -0
  148. package/dist/host/profile/accounts.js +2 -4
  149. package/dist/host/profile/claude-login.d.ts +3 -2
  150. package/dist/host/profile/claude-login.js +3 -13
  151. package/dist/host/sandbox/cleanup.d.ts +37 -0
  152. package/dist/host/sandbox/cleanup.js +82 -0
  153. package/dist/host/sandbox/index.d.ts +1 -0
  154. package/dist/host/sandbox/index.js +1 -0
  155. package/dist/host/sandbox/platform.js +1 -1
  156. package/dist/host/vpn/index.d.ts +2 -1
  157. package/dist/host/vpn/index.js +2 -1
  158. package/dist/host/vpn/last.d.ts +28 -0
  159. package/dist/host/vpn/last.js +43 -0
  160. package/dist/host/vpn/login.d.ts +3 -1
  161. package/dist/host/vpn/login.js +17 -3
  162. package/dist/runtime/agent.js +31 -1
  163. package/dist/runtime/daemon.js +1419 -1197
  164. package/dist/runtime/db.js +33 -1
  165. package/dist/runtime/main.js +1311 -1037
  166. package/dist/runtime/netproxy.js +25 -1
  167. package/dist/runtime/tool.js +241 -209
  168. package/dist/runtime/view.js +25 -1
  169. package/dist/selftest/agent-commands.test.js +45 -7
  170. package/dist/selftest/agent-paths.test.js +1 -12
  171. package/dist/selftest/agent-settings.test.d.ts +2 -0
  172. package/dist/selftest/agent-settings.test.js +94 -0
  173. package/dist/selftest/app.test.js +2 -1
  174. package/dist/selftest/cli.test.js +4 -0
  175. package/dist/selftest/heavy-load.test.js +119 -2
  176. package/dist/selftest/index.js +2 -0
  177. package/dist/selftest/menu.test.js +5 -3
  178. package/dist/selftest/pc-installs.test.js +5 -0
  179. package/dist/selftest/persisted-installs.test.js +69 -0
  180. package/dist/selftest/service-accounts.test.js +1 -1
  181. package/dist/selftest/support/engine-golden.js +1 -1
  182. package/dist/selftest/support/fixtures.d.ts +9 -0
  183. package/dist/selftest/vpn.test.js +51 -3
  184. package/dist/shared/accounts/anthropic.d.ts +5 -0
  185. package/dist/shared/accounts/anthropic.js +15 -0
  186. package/dist/shared/accounts/index.d.ts +1 -0
  187. package/dist/shared/accounts/index.js +1 -0
  188. package/dist/shared/channel/schema.d.ts +20 -0
  189. package/dist/shared/channel/schema.js +4 -1
  190. package/dist/shared/io/disk.d.ts +9 -0
  191. package/dist/shared/io/disk.js +19 -0
  192. package/dist/shared/io/index.d.ts +1 -0
  193. package/dist/shared/io/index.js +1 -0
  194. package/dist/shared/readiness.d.ts +9 -0
  195. package/dist/shared/readiness.js +11 -0
  196. package/dist/shared/signer.d.ts +24 -0
  197. package/dist/shared/signer.js +15 -0
  198. package/dist/shared/system.d.ts +1 -0
  199. package/dist/shared/system.js +1 -0
  200. package/dist/shared/vpn/failure.d.ts +10 -0
  201. package/dist/shared/vpn/failure.js +28 -0
  202. package/dist/sync/watcher.js +11 -4
  203. package/dist/web/index.html +1 -1
  204. package/dist/web/{main-6XYO5FZC.js → main-OUUQYNK4.js} +75 -74
  205. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  206. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  207. package/native/csharp/HostTools/Program.cs +19 -2
  208. package/native/csharp/HostTools/UserPath.cs +17 -0
  209. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  210. package/native/scripts/edit-user-path.ps1 +6 -2
  211. package/native/scripts/stop-events.ps1 +7 -3
  212. package/package.json +1 -1
  213. package/sandbox/scripts/get-signature.ps1 +13 -0
  214. package/schemas/config.schema.json +64 -0
  215. package/schemas/sandbox-config.schema.json +8 -0
  216. package/sandbox/scripts/get-signer.ps1 +0 -8
@@ -10,15 +10,20 @@ import path from 'node:path';
10
10
  import { z } from 'zod';
11
11
  import { spawnAsync } from '../../../shared/download/index.js';
12
12
  import { writeJsonAtomic } from '../../../shared/io/json.js';
13
+ import { robocopyFailed } from '../../../shared/io/robocopy-result.js';
14
+ import { safeRemove } from '../../../shared/io/safe-remove.js';
15
+ import { SYSTEM } from '../../../shared/system.js';
13
16
  import { execOutput } from '../../runtime/process.js';
14
17
  import { exists, mkdirp } from '../../runtime/fs.js';
15
18
  import { log, warn } from '../../runtime/log.js';
19
+ import { logsDir } from '../../runtime/paths.js';
16
20
  import { duringSession } from '../../runtime/project-context.js';
17
21
  import { errMsg } from '../../../core/util/errors.js';
18
22
  import { SIGNERS, signedInstaller } from '../installers.js';
19
- import { assertLocalRoom, freeBytes } from '../persisted.js';
23
+ import { assertLocalRoom, freeBytes, LOCAL_RESERVE_BYTES } from '../persisted.js';
20
24
  import { tool } from '../tool-env.js';
21
- export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted } from './vs-request.js';
25
+ import { layoutNextStep, vsExitText } from './vs-request.js';
26
+ export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, unrealUsesAutosdk, visualStudioWanted, } from './vs-request.js';
22
27
  const BOOTSTRAPPERS = {
23
28
  community: {
24
29
  url: 'https://aka.ms/vs/17/release/vs_community.exe',
@@ -63,6 +68,61 @@ export function installedVs() {
63
68
  function layoutDir(product) {
64
69
  return path.join(VS_DIR, product === 'community' ? 'layout' : 'layout-buildtools');
65
70
  }
71
+ /** The layout arguments of the bootstrapper (the same for the data disk and the staging folder). */
72
+ const layoutArgs = (req, folder) => [
73
+ '--layout',
74
+ folder,
75
+ '--lang',
76
+ 'en-US',
77
+ '--quiet',
78
+ '--wait',
79
+ ...req.components.flatMap((id) => ['--add', id]),
80
+ ];
81
+ /** The bootstrapper's newest log (dd_bootstrapper_*.log) in these folders, or null. */
82
+ function newestBootstrapperLog(folders) {
83
+ const logs = folders.flatMap((dir) => {
84
+ try {
85
+ return fs
86
+ .readdirSync(dir)
87
+ .filter((n) => /^dd_bootstrapper_.*\.log$/i.test(n))
88
+ .map((n) => path.join(dir, n));
89
+ }
90
+ catch {
91
+ return []; // The folder is not there: no log in it.
92
+ }
93
+ });
94
+ const stamp = (f) => {
95
+ try {
96
+ return fs.statSync(f).mtimeMs;
97
+ }
98
+ catch {
99
+ return 0; // Gone since the listing.
100
+ }
101
+ };
102
+ return logs.sort((x, y) => stamp(y) - stamp(x))[0] ?? null;
103
+ }
104
+ /** Says why the layout failed and keeps the installer's log where the sandbox's logs are. */
105
+ function reportLayoutFailure(code, temps) {
106
+ warn('Visual Studio: saving the install files failed (exit ' + code + '): ' + vsExitText(code));
107
+ const found = newestBootstrapperLog(temps);
108
+ if (!found)
109
+ return;
110
+ const kept = path.join(logsDir, 'vs-layout-' + new Date().toISOString().slice(0, 10) + '.log');
111
+ try {
112
+ fs.mkdirSync(logsDir, { recursive: true });
113
+ fs.copyFileSync(found, kept);
114
+ warn('Visual Studio: the installer log is ' + found + ' (a copy is in ' + kept + ')');
115
+ }
116
+ catch (e) {
117
+ warn('Visual Studio: the installer log is ' + found + ' (could not copy it: ' + errMsg(e) + ')');
118
+ }
119
+ }
120
+ /** Copies a finished layout onto the data disk (never /MOV: mapped folders refuse rename-over). */
121
+ async function copyLayout(from, to) {
122
+ mkdirp(to);
123
+ const code = await spawnAsync(SYSTEM.robocopy(), [from, to, '/E', '/COPY:DAT', '/R:1', '/W:1', '/MT:8', '/NP']);
124
+ return !robocopyFailed(code);
125
+ }
66
126
  /** The offline copy's installer when it holds every component; made or completed when there is room. */
67
127
  async function layoutInstaller(req, bootstrapper) {
68
128
  const layout = layoutDir(req.product);
@@ -79,6 +139,10 @@ async function layoutInstaller(req, bootstrapper) {
79
139
  return false;
80
140
  }
81
141
  };
142
+ const finish = () => {
143
+ writeJsonAtomic(marker, { components: [...req.components] });
144
+ return installer;
145
+ };
82
146
  if (has())
83
147
  return installer;
84
148
  if (freeBytes(mkdirp(VS_DIR)) < LAYOUT_ROOM_BYTES) {
@@ -87,21 +151,36 @@ async function layoutInstaller(req, bootstrapper) {
87
151
  }
88
152
  log('Saving the Visual Studio install files for later boots (~10 GB, once) ...');
89
153
  fs.rmSync(marker, { force: true });
90
- const code = await spawnAsync(bootstrapper, [
91
- '--layout',
92
- layout,
93
- '--lang',
94
- 'en-US',
95
- '--quiet',
96
- '--wait',
97
- ...req.components.flatMap((id) => ['--add', id]),
98
- ]);
99
- if (!OK_EXIT_CODES.includes(code) || !exists(installer)) {
100
- warn('Visual Studio: saving the install files failed (exit ' + code + '); downloading this boot');
154
+ // (a) The installer writes its scratch files to TEMP and moves them into the layout: a TEMP on the same (mapped)
155
+ // volume, inside the layout's folder tree, keeps those moves on one volume.
156
+ const scratch = mkdirp(path.join(VS_DIR, 'tmp'));
157
+ const code = await spawnAsync(bootstrapper, layoutArgs(req, layout), {
158
+ env: { ...process.env, TEMP: scratch, TMP: scratch },
159
+ });
160
+ if (OK_EXIT_CODES.includes(code) && exists(installer))
161
+ return finish();
162
+ reportLayoutFailure(code, [scratch, os.tmpdir()]);
163
+ // (b) Build it on the sandbox's own disk instead, then copy it over.
164
+ const drive = (process.env.SystemDrive ?? 'C:') + '\\';
165
+ if (layoutNextStep(code, freeBytes(drive), LOCAL_RESERVE_BYTES) !== 'stage') {
166
+ warn('Visual Studio: not enough room on ' + drive + ' to build the install files there; downloading this boot');
101
167
  return null;
102
168
  }
103
- writeJsonAtomic(marker, { components: [...req.components] });
104
- return installer;
169
+ const staging = path.join(drive, 'vs-layout-staging');
170
+ log('Building the install files on ' + drive + ' and copying them to ' + layout + ' ...');
171
+ const stagedCode = await spawnAsync(bootstrapper, layoutArgs(req, staging));
172
+ const staged = path.join(staging, BOOTSTRAPPERS[req.product].file);
173
+ let kept = false;
174
+ if (OK_EXIT_CODES.includes(stagedCode) && exists(staged)) {
175
+ kept = await copyLayout(staging, layout);
176
+ if (!kept)
177
+ warn('Visual Studio: copying the install files to ' + layout + ' failed');
178
+ }
179
+ else {
180
+ reportLayoutFailure(stagedCode, [os.tmpdir()]);
181
+ }
182
+ safeRemove(staging, drive);
183
+ return kept && exists(installer) ? finish() : null;
105
184
  }
106
185
  async function install(req, keepInstallFiles) {
107
186
  const boot = BOOTSTRAPPERS[req.product];
@@ -125,7 +204,12 @@ async function install(req, keepInstallFiles) {
125
204
  ...req.components.flatMap((id) => ['--add', id]),
126
205
  ]);
127
206
  if (!OK_EXIT_CODES.includes(code) || !installedVs())
128
- throw new Error('the Visual Studio installer failed (exit ' + code + '); its logs are the dd_*.log files in ' + os.tmpdir());
207
+ throw new Error('the Visual Studio installer failed (exit ' +
208
+ code +
209
+ ': ' +
210
+ vsExitText(code) +
211
+ '); its logs are the dd_*.log files in ' +
212
+ os.tmpdir());
129
213
  }
130
214
  /** Visual Studio as asked for: installed at boot when it is not there yet; its folder and version. */
131
215
  export async function ensureVisualStudio(env, req, o) {
@@ -17,3 +17,24 @@ export interface VsRequest {
17
17
  readonly accepted: boolean;
18
18
  }
19
19
  export declare function visualStudioWanted(t: Toolchains, on?: (setting: boolean | 'auto', name: string) => boolean): VsRequest | null;
20
+ /** Whether Unreal is on and builds from the portable MSVC through AutoSDK instead of Visual Studio. */
21
+ export declare function unrealUsesAutosdk(t: Toolchains, on?: (setting: boolean | 'auto', name: string) => boolean): boolean;
22
+ /** The Visual Studio installer's exit code in plain words (it is a uint32: -1 and 4294967295 are the same). */
23
+ export declare function vsExitText(code: number): string;
24
+ /** Room the staging copy of the layout needs on the sandbox's own disk, on top of the reserve. */
25
+ export declare const LAYOUT_STAGING_BYTES: number;
26
+ /**
27
+ * What to do after the layout attempt in the data disk: 'done' when it worked, 'stage' (build it on the sandbox's
28
+ * own disk and copy it over) when it failed and that disk has room, 'skip' (download this boot) otherwise.
29
+ */
30
+ export declare function layoutNextStep(exit: number | null, localFreeBytes: number, reserveBytes: number): 'done' | 'stage' | 'skip';
31
+ export interface CopyPair {
32
+ readonly from: string;
33
+ readonly to: string;
34
+ }
35
+ /**
36
+ * The copies that make an Unreal AutoSDK tree from the portable MSVC (copies, not links: mapped folders refuse
37
+ * junctions): MSVC's tools into HostWin64\Win64\VS2022\<msvc> and the Windows SDK's Include, Lib and bin folders into
38
+ * HostWin64\Win64\Windows Kits\10. Pure.
39
+ */
40
+ export declare function autosdkLayout(msvcRoot: string, msvc: string, sdk: string, autosdkRoot: string): CopyPair[];
@@ -1,3 +1,8 @@
1
+ // Which Visual Studio this sandbox installs (a real installer: it runs every boot), from every toolchain that needs
2
+ // one: Unreal (Community with the C++ game workload), Flutter's Windows desktop builds and Unity's Windows IL2CPP
3
+ // builds (the Build Tools' C++ parts). Pure: no runtime, so the self-test loads it. Community holds everything the
4
+ // Build Tools would add, so one install serves all of them.
5
+ import path from 'node:path';
1
6
  /**
2
7
  * What Epic lists for Unreal Engine 5 with Visual Studio 2022: the game workload, MSVC v143, a Windows SDK of
3
8
  * 10.0.18362 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
@@ -22,7 +27,8 @@ export const BUILD_TOOLS_COMPONENTS = [
22
27
  /** Whether a toolchain is on (the host resolved "auto"; a session file that still says "auto" counts as off). */
23
28
  const isOn = (setting) => setting === true;
24
29
  export function visualStudioWanted(t, on = isOn) {
25
- const unreal = on(t.unreal.enabled, 'unreal');
30
+ // Unreal with visualStudio "autosdk" needs no Visual Studio: it builds from the portable MSVC (autosdk.ts).
31
+ const unreal = on(t.unreal.enabled, 'unreal') && t.unreal.visualStudio === 'full';
26
32
  const flutter = on(t.flutter.enabled, 'flutter') && t.flutter.windowsDesktop;
27
33
  const unity = on(t.unity.enabled, 'unity') && t.unity.modules.includes('windows-il2cpp');
28
34
  const users = [...(unreal ? ['unreal'] : []), ...(flutter ? ['flutter'] : []), ...(unity ? ['unity'] : [])];
@@ -44,3 +50,56 @@ export function visualStudioWanted(t, on = isOn) {
44
50
  accepted: t.msvc.acceptLicense,
45
51
  };
46
52
  }
53
+ /** Whether Unreal is on and builds from the portable MSVC through AutoSDK instead of Visual Studio. */
54
+ export function unrealUsesAutosdk(t, on = isOn) {
55
+ return on(t.unreal.enabled, 'unreal') && t.unreal.visualStudio === 'autosdk';
56
+ }
57
+ /** The Visual Studio installer's exit code in plain words (it is a uint32: -1 and 4294967295 are the same). */
58
+ export function vsExitText(code) {
59
+ const c = code >>> 0;
60
+ switch (c) {
61
+ case 0:
62
+ return 'installed';
63
+ case 3010:
64
+ return 'done; a restart would finish it (not needed here)';
65
+ case 1602:
66
+ return 'cancelled';
67
+ case 5007:
68
+ return 'the installer refused to run (blocked by Windows, for example a restart waiting)';
69
+ case 8006:
70
+ return 'Visual Studio is running and must be closed first';
71
+ case 0xe0434352:
72
+ return '0xE0434352: the Visual Studio installer crashed';
73
+ default:
74
+ return 'the installer failed with exit ' + c + ' (0x' + c.toString(16).toUpperCase() + ')';
75
+ }
76
+ }
77
+ /** Room the staging copy of the layout needs on the sandbox's own disk, on top of the reserve. */
78
+ export const LAYOUT_STAGING_BYTES = 12 * 1024 ** 3;
79
+ /**
80
+ * What to do after the layout attempt in the data disk: 'done' when it worked, 'stage' (build it on the sandbox's
81
+ * own disk and copy it over) when it failed and that disk has room, 'skip' (download this boot) otherwise.
82
+ */
83
+ export function layoutNextStep(exit, localFreeBytes, reserveBytes) {
84
+ if (exit !== null && (exit === 0 || exit === 3010))
85
+ return 'done';
86
+ return localFreeBytes >= LAYOUT_STAGING_BYTES + reserveBytes ? 'stage' : 'skip';
87
+ }
88
+ /**
89
+ * The copies that make an Unreal AutoSDK tree from the portable MSVC (copies, not links: mapped folders refuse
90
+ * junctions): MSVC's tools into HostWin64\Win64\VS2022\<msvc> and the Windows SDK's Include, Lib and bin folders into
91
+ * HostWin64\Win64\Windows Kits\10. Pure.
92
+ */
93
+ export function autosdkLayout(msvcRoot, msvc, sdk, autosdkRoot) {
94
+ const win64 = path.win32.join(autosdkRoot, 'HostWin64', 'Win64');
95
+ return [
96
+ {
97
+ from: path.win32.join(msvcRoot, 'VC', 'Tools', 'MSVC', msvc),
98
+ to: path.win32.join(win64, 'VS2022', msvc),
99
+ },
100
+ ...['Include', 'Lib', 'bin'].map((part) => ({
101
+ from: path.win32.join(msvcRoot, 'Windows Kits', '10', part, sdk),
102
+ to: path.win32.join(win64, 'Windows Kits', '10', part, sdk),
103
+ })),
104
+ ];
105
+ }
@@ -1,2 +1,2 @@
1
1
  import type { ToolEnv } from './tool-env.js';
2
- export declare function notAvailableYet(env: ToolEnv, what: string): void;
2
+ export declare function notAvailableYet(_env: ToolEnv, what: string): void;
@@ -1,6 +1,6 @@
1
- // What a toolchain step whose module is not written yet does: says so in the log and the summary, installs nothing.
1
+ // What a toolchain step whose module is not written yet does: says so in the log, installs nothing and adds no
2
+ // summary line.
2
3
  import { log } from '../runtime/log.js';
3
- export function notAvailableYet(env, what) {
4
+ export function notAvailableYet(_env, what) {
4
5
  log(what + ': not available yet (nothing installed)');
5
- env.summary.push(what + ': not available yet');
6
6
  }
@@ -7,6 +7,7 @@
7
7
  import crypto from 'node:crypto';
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
+ import { freeBytesAt } from '../../shared/io/disk.js';
10
11
  import { readValid, writeJsonAtomic } from '../../shared/io/json.js';
11
12
  import { safeRemove } from '../../shared/io/safe-remove.js';
12
13
  import { dataDir } from '../runtime/paths.js';
@@ -27,13 +28,7 @@ function checkSpec(spec) {
27
28
  }
28
29
  /** Free bytes for this user on the disk holding `dir` (0 when it cannot be told). */
29
30
  export function freeBytes(dir, deps = {}) {
30
- try {
31
- const s = (deps.statfs ?? fs.statfsSync)(dir);
32
- return s.bavail * s.bsize;
33
- }
34
- catch {
35
- return 0;
36
- }
31
+ return freeBytesAt(dir, deps.statfs) ?? 0;
37
32
  }
38
33
  const gb = (bytes) => (bytes / GB).toFixed(1) + ' GB';
39
34
  /** Throws (with the fix) when the disk holding `dir` has less than `needBytes` free. */
@@ -21,7 +21,7 @@ import { setupPhp } from './languages/php.js';
21
21
  import { setupRuby } from './languages/ruby.js';
22
22
  import { setupVcRuntime } from './native/vcruntime.js';
23
23
  import { setupVscode } from './tools/vscode.js';
24
- import { ensureVisualStudio, visualStudioWanted } from './native/visual-studio.js';
24
+ import { ensureVisualStudio, unrealUsesAutosdk, visualStudioWanted } from './native/visual-studio.js';
25
25
  import { duringSession } from '../runtime/project-context.js';
26
26
  import { setupSteam } from './steam/steam.js';
27
27
  import { setupGodot } from './engines/godot.js';
@@ -42,6 +42,8 @@ export function plan(config, versions, o) {
42
42
  const android = wanted(config.android.enabled, 'android');
43
43
  // One Visual Studio for every toolchain that needs the real one (Unreal, Flutter desktop, Unity IL2CPP).
44
44
  const vs = visualStudioWanted(config, (setting, name) => wanted(setting, name));
45
+ // Unreal on the portable MSVC (AutoSDK) needs that MSVC even when nothing else asked for it.
46
+ const autosdk = unrealUsesAutosdk(config, (setting, name) => wanted(setting, name));
45
47
  const ctx = {
46
48
  refresh: o.refresh,
47
49
  pcInstalls: o.pcInstalls ?? { items: [] },
@@ -77,13 +79,16 @@ export function plan(config, versions, o) {
77
79
  name: 'Unreal',
78
80
  enabled: unreal,
79
81
  shared: true,
80
- after: ['Visual Studio'],
82
+ after: ['Visual Studio', 'MSVC'],
81
83
  install: (env) => setupUnreal(env, config.unreal, ctx),
82
84
  },
83
85
  {
84
86
  name: 'MSVC',
85
- enabled: wanted(config.msvc.enabled, 'msvc') && vs === null,
86
- install: (env) => setupMsvc(env, config.msvc),
87
+ enabled: (wanted(config.msvc.enabled, 'msvc') || (autosdk && config.msvc.enabled !== false)) && vs === null,
88
+ // The same Visual Studio licence terms, which Unreal's acceptLicense already names.
89
+ install: (env) => setupMsvc(env, autosdk
90
+ ? { ...config.msvc, acceptLicense: config.msvc.acceptLicense || config.unreal.acceptLicense }
91
+ : config.msvc),
87
92
  },
88
93
  // Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
89
94
  {
@@ -0,0 +1,5 @@
1
+ import { type AppSigner, type Signature } from '../../shared/signer.js';
2
+ /** The names on a file's valid signature, or null when it is unsigned, tampered with, or cannot be read. */
3
+ export declare function signatureOf(file: string): Signature | null;
4
+ /** Whether a file's valid signature is the expected signer's. */
5
+ export declare function signedBy(file: string, signer: AppSigner): boolean;
@@ -0,0 +1,22 @@
1
+ // A file's Authenticode signature (sandbox\scripts\get-signature.ps1) checked against the signer a download names.
2
+ import { z } from 'zod';
3
+ import { signerMatches } from '../../shared/signer.js';
4
+ import { scriptOutput } from '../runtime/process.js';
5
+ const SignatureSchema = z.object({ subject: z.string().max(4096), issuer: z.string().max(4096) });
6
+ /** The names on a file's valid signature, or null when it is unsigned, tampered with, or cannot be read. */
7
+ export function signatureOf(file) {
8
+ const text = scriptOutput('get-signature.ps1', ['-Path', file]).trim();
9
+ if (!text)
10
+ return null;
11
+ try {
12
+ const parsed = SignatureSchema.safeParse(JSON.parse(text));
13
+ return parsed.success ? parsed.data : null;
14
+ }
15
+ catch {
16
+ return null; // Not the script's JSON: treated as unsigned.
17
+ }
18
+ }
19
+ /** Whether a file's valid signature is the expected signer's. */
20
+ export function signedBy(file, signer) {
21
+ return signerMatches(signatureOf(file), signer);
22
+ }
@@ -1,6 +1,9 @@
1
+ import { type VpnFailureCode } from '../../shared/vpn/failure.js';
1
2
  export interface TunnelOutcome {
2
3
  readonly ok: boolean;
3
4
  readonly message: string;
5
+ /** Why it failed (when it did), for the host to act on. */
6
+ readonly code?: VpnFailureCode;
4
7
  /** The tunnel's own address (what the kill switch lets through). */
5
8
  readonly tunnelIp?: string;
6
9
  }
@@ -6,21 +6,14 @@ import { spawn } from 'node:child_process';
6
6
  import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
+ import { classifyFailure } from '../../shared/vpn/failure.js';
9
10
  import { tunnelAddressFromLog } from '../../shared/vpn/kill-switch.js';
10
11
  import { logsDir } from '../runtime/paths.js';
11
12
  import { log } from '../runtime/log.js';
12
13
  const DONE = 'Initialization Sequence Completed';
13
14
  /** Why the log says OpenVPN stopped, in a short plain sentence (or null when it does not say). */
14
15
  export function failureReason(text) {
15
- if (/AUTH_FAILED/i.test(text))
16
- return 'the VPN refused the login (for NordVPN use the service credentials, not your account password)';
17
- if (/TLS Error: TLS key negotiation failed|TLS handshake failed/i.test(text))
18
- return 'it could not reach the VPN server (the network blocks it, or the server is down)';
19
- if (/All wintun adapters on this system are in use|Cannot open TUN\/TAP|Could not create Wintun|ovpn-dco.*(error|failed)/i.test(text))
20
- return 'the network driver for the tunnel could not be used';
21
- if (/Exiting due to fatal error/i.test(text))
22
- return 'OpenVPN stopped with an error';
23
- return null;
16
+ return classifyFailure(text)?.reason ?? null;
24
17
  }
25
18
  const tail = (text, lines = 12) => text.trim().split(/\r?\n/).slice(-lines).join('\n');
26
19
  /** Connects. Never leaves the login on disk; kills OpenVPN when the tunnel does not come up in time. */
@@ -57,17 +50,22 @@ export async function startTunnel(o) {
57
50
  const tunnelIp = tunnelAddressFromLog(text) ?? undefined;
58
51
  return { ok: true, message: 'the tunnel is up', ...(tunnelIp ? { tunnelIp } : {}) };
59
52
  }
60
- const reason = failureReason(text);
53
+ const failure = classifyFailure(text);
54
+ const reason = failure?.reason ?? null;
61
55
  if (exited !== null || reason) {
62
56
  if (exited === null)
63
57
  child.kill();
64
58
  keepLog();
65
- return { ok: false, message: (reason ?? 'OpenVPN stopped') + '. ' + tail(text, 4) };
59
+ return {
60
+ ok: false,
61
+ code: failure?.code ?? 'other',
62
+ message: (reason ?? 'OpenVPN stopped') + '. ' + tail(text, 4),
63
+ };
66
64
  }
67
65
  if (Date.now() > deadline) {
68
66
  child.kill();
69
67
  keepLog();
70
- return { ok: false, message: 'the tunnel did not come up in time. ' + tail(text, 4) };
68
+ return { ok: false, code: 'timeout', message: 'the tunnel did not come up in time. ' + tail(text, 4) };
71
69
  }
72
70
  await new Promise((r) => setTimeout(r, 500));
73
71
  }
@@ -123,6 +123,7 @@ export async function setupVpn(secrets, ptyHostExe) {
123
123
  warn('VPN: the kill switch did not hold: a direct connection still got out. If the tunnel drops, traffic leaves through your normal connection.');
124
124
  };
125
125
  setVpnStatus('starting', 'Starting the VPN');
126
+ let failureCode;
126
127
  try {
127
128
  if (!session.networking)
128
129
  throw new Error('networking is off for this sandbox');
@@ -178,6 +179,7 @@ export async function setupVpn(secrets, ptyHostExe) {
178
179
  if (!result.ok) {
179
180
  if (wireguardExe)
180
181
  await removeWireguardTunnel(wireguardExe);
182
+ failureCode = result.code;
181
183
  throw new Error(result.message);
182
184
  }
183
185
  if (!result.tunnelIp || !hasAdapter(result.tunnelIp))
@@ -207,7 +209,10 @@ export async function setupVpn(secrets, ptyHostExe) {
207
209
  ? ' The kill switch is on, so this sandbox has NO internet access.'
208
210
  : ' The kill switch could NOT close the network, so this sandbox is ONLINE through your normal connection, without the VPN.';
209
211
  warn('VPN failed: ' + reason + after);
210
- setVpnStatus('failed', reason, killSwitch ? { killSwitchHeld: held } : {});
212
+ setVpnStatus('failed', reason, {
213
+ code: failureCode ?? 'other',
214
+ ...(killSwitch ? { killSwitchHeld: held } : {}),
215
+ });
211
216
  return false;
212
217
  }
213
218
  }
@@ -3,13 +3,13 @@ import { type ServiceKind } from '../../shared/accounts/index.js';
3
3
  declare const LeaseSchema: z.ZodObject<{
4
4
  kind: z.ZodEnum<{
5
5
  unity: "unity";
6
- steam: "steam";
7
- epic: "epic";
8
- cfx: "cfx";
9
6
  keystore: "keystore";
10
7
  play: "play";
11
8
  expo: "expo";
12
9
  appstore: "appstore";
10
+ cfx: "cfx";
11
+ steam: "steam";
12
+ epic: "epic";
13
13
  }>;
14
14
  dataSet: z.ZodString;
15
15
  launchId: z.ZodString;
@@ -11,5 +11,5 @@ export function pcEnv() {
11
11
  }
12
12
  /** What a kind whose host side is not written yet says. */
13
13
  export function notAvailableYet(kind) {
14
- return fail('Saving a ' + SERVICE_SPECS[kind].label + ' sign-in is not available yet.', 'It comes in a later version of the sandbox.');
14
+ return fail('Saving a ' + SERVICE_SPECS[kind].label + ' sign-in comes in a later version of the sandbox.', 'Nothing was changed.');
15
15
  }
@@ -1,4 +1,4 @@
1
- // (Beta) Agent commands: with agentCommands.enabled, typing an agent's own command on this PC (claude, codex, ...)
1
+ // Agent commands: with agentCommands.enabled, typing an agent's own command on this PC (claude, codex, ...)
2
2
  // starts it in a hidden sandbox for the folder you are in and attaches that terminal. Small forwarding files in
3
3
  // <storage>\agent-commands go first on YOUR PATH (so they win over npm's and the installers' copies); turning it off
4
4
  // removes the files and that one PATH entry. SANDBOX_AGENT_DIRECT=1 runs the real agent on this PC instead.
@@ -1,7 +1,19 @@
1
1
  import type { AgentCommand } from './commands.js';
2
2
  /** Marks files this tool wrote, so only those are ever replaced or removed. */
3
3
  export declare const MARKER = "windows-development-sandbox agent command";
4
+ /** What a forwarding file runs: the managed install's launcher when there is one, else node + the entry file. */
5
+ export interface ForwardTarget {
6
+ readonly node: string;
7
+ readonly entry: string;
8
+ /** The managed install's `bin\sandbox.cmd`: it survives updates, which remove the version folder `entry` is in. */
9
+ readonly launcher?: string;
10
+ }
11
+ /**
12
+ * Where the forwarding files send the typed command. Under a managed install (installRoot, the folder with `bin`,
13
+ * `versions` and `node`) that is its launcher; otherwise the entry file of this copy.
14
+ */
15
+ export declare function forwardTarget(packageRoot: string, installRoot: string | null, node: string): ForwardTarget;
4
16
  /** The .cmd file for one agent (cmd, PowerShell and Windows Terminal all run it). */
5
- export declare function forwardingCmd(a: AgentCommand, node: string, entry: string): string;
17
+ export declare function forwardingCmd(a: AgentCommand, target: ForwardTarget): string;
6
18
  /** The same for Git Bash, which looks for a file without an extension. */
7
- export declare function forwardingSh(a: AgentCommand, node: string, entry: string): string;
19
+ export declare function forwardingSh(a: AgentCommand, target: ForwardTarget): string;
@@ -1,12 +1,30 @@
1
+ import path from 'node:path';
1
2
  /** Marks files this tool wrote, so only those are ever replaced or removed. */
2
3
  export const MARKER = 'windows-development-sandbox agent command';
4
+ /**
5
+ * Where the forwarding files send the typed command. Under a managed install (installRoot, the folder with `bin`,
6
+ * `versions` and `node`) that is its launcher; otherwise the entry file of this copy.
7
+ */
8
+ export function forwardTarget(packageRoot, installRoot, node) {
9
+ const entry = path.join(packageRoot, 'dist', 'bin', 'sandbox.js');
10
+ return installRoot ? { node, entry, launcher: path.join(installRoot, 'bin', 'sandbox.cmd') } : { node, entry };
11
+ }
12
+ const posix = (p) => p.replace(/^([A-Za-z]):/, (_m, d) => '/' + d.toLowerCase()).replace(/\\/g, '/');
3
13
  /** The .cmd file for one agent (cmd, PowerShell and Windows Terminal all run it). */
4
- export function forwardingCmd(a, node, entry) {
14
+ export function forwardingCmd(a, target) {
15
+ const launch = ' launch --harness ' + a.harness + ' --headless --terminal -- %*';
5
16
  return [
6
17
  '@echo off',
7
- 'rem ' + MARKER + ' (Beta). Turn off: sandbox agent-commands off. Run the real one: set SANDBOX_AGENT_DIRECT=1',
18
+ 'rem ' + MARKER + '. Turn off: sandbox agent-commands off. Run the real one: set SANDBOX_AGENT_DIRECT=1',
8
19
  'if "%SANDBOX_AGENT_DIRECT%"=="1" goto direct',
9
- '"' + node + '" "' + entry + '" launch --harness ' + a.harness + ' --headless --terminal -- %*',
20
+ // A missing program (the tool was uninstalled or moved) runs the real agent instead of failing.
21
+ ...(target.launcher
22
+ ? ['if not exist "' + target.launcher + '" goto direct', 'call "' + target.launcher + '"' + launch]
23
+ : [
24
+ 'if not exist "' + target.node + '" goto direct',
25
+ 'if not exist "' + target.entry + '" goto direct',
26
+ '"' + target.node + '" "' + target.entry + '"' + launch,
27
+ ]),
10
28
  'exit /b %errorlevel%',
11
29
  ':direct',
12
30
  'call "' + a.real + '" %*',
@@ -14,20 +32,20 @@ export function forwardingCmd(a, node, entry) {
14
32
  ].join('\r\n');
15
33
  }
16
34
  /** The same for Git Bash, which looks for a file without an extension. */
17
- export function forwardingSh(a, node, entry) {
18
- const posix = (p) => p.replace(/^([A-Za-z]):/, (_m, d) => '/' + d.toLowerCase()).replace(/\\/g, '/');
35
+ export function forwardingSh(a, target) {
19
36
  const q = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
37
+ const launch = ' launch --harness ' + a.harness + ' --headless --terminal -- "$@"';
38
+ const shLauncher = target.launcher ? posix(target.launcher).replace(/\.cmd$/i, '') : null;
20
39
  return [
21
40
  '#!/bin/sh',
22
- '# ' + MARKER + ' (Beta). Turn off: sandbox agent-commands off. Run the real one: SANDBOX_AGENT_DIRECT=1',
41
+ '# ' + MARKER + '. Turn off: sandbox agent-commands off. Run the real one: SANDBOX_AGENT_DIRECT=1',
23
42
  'if [ "$SANDBOX_AGENT_DIRECT" = "1" ]; then exec ' + q(posix(a.real)) + ' "$@"; fi',
24
- 'exec ' +
25
- q(posix(node)) +
26
- ' ' +
27
- q(posix(entry)) +
28
- ' launch --harness ' +
29
- a.harness +
30
- ' --headless --terminal -- "$@"',
43
+ ...(shLauncher
44
+ ? ['[ -f ' + q(shLauncher) + ' ] || exec ' + q(posix(a.real)) + ' "$@"', 'exec ' + q(shLauncher) + launch]
45
+ : [
46
+ '[ -f ' + q(posix(target.entry)) + ' ] || exec ' + q(posix(a.real)) + ' "$@"',
47
+ 'exec ' + q(posix(target.node)) + ' ' + q(posix(target.entry)) + launch,
48
+ ]),
31
49
  '',
32
50
  ].join('\n');
33
51
  }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from '../../core/types.js';
2
+ import { type ForwardTarget } from './forwarders.js';
2
3
  import { type AgentCommand } from './commands.js';
3
4
  export interface Reconciled {
4
5
  on: boolean;
@@ -9,4 +10,4 @@ export interface Reconciled {
9
10
  * Makes this PC match agentCommands: on = a forwarding file per installed agent and the folder first on your PATH;
10
11
  * off = no files, no PATH entry. Only touches files it wrote and its own PATH entry; does nothing when already right.
11
12
  */
12
- export declare function reconcileAgentCommands(config: Config, storageRoot: string, node: string, entry: string): Reconciled;
13
+ export declare function reconcileAgentCommands(config: Config, storageRoot: string, target: ForwardTarget): Reconciled;