windows-development-sandbox 1.16.0 → 1.17.3

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 (64) hide show
  1. package/README.md +92 -92
  2. package/config.example.json +6 -0
  3. package/dist/app/features/access/access.run.js +1 -1
  4. package/dist/bin/agent-sandbox.js +2 -2
  5. package/dist/bin/{chunk-7EXDUNE7.js → chunk-QLGGAG3D.js} +1 -1
  6. package/dist/bin/{chunk-HIC4CUMY.js → chunk-REE3EMHD.js} +73 -16
  7. package/dist/bin/{chunk-PXTLKIV4.js → chunk-XYUVIVJT.js} +1 -1
  8. package/dist/bin/claude-host.js +2 -2
  9. package/dist/bin/claude-sbx.js +2 -2
  10. package/dist/bin/codex-sbx.js +2 -2
  11. package/dist/bin/gemini-sbx.js +2 -2
  12. package/dist/bin/sandbox.js +2 -2
  13. package/dist/bin/sbx-hide.js +2 -2
  14. package/dist/bin/sbx-show.js +2 -2
  15. package/dist/cli/commands/web.js +1 -1
  16. package/dist/core/config/defaults.js +1 -0
  17. package/dist/core/detect/toolchains.d.ts +1 -0
  18. package/dist/core/detect/toolchains.js +7 -0
  19. package/dist/core/schema/config.d.ts +21 -0
  20. package/dist/core/schema/sandbox.js +1 -1
  21. package/dist/core/schema/toolchains.d.ts +6 -0
  22. package/dist/core/schema/toolchains.js +10 -0
  23. package/dist/guest/agents/agent-files.d.ts +6 -0
  24. package/dist/guest/agents/agent-files.js +14 -0
  25. package/dist/guest/agents/local-state.d.ts +2 -0
  26. package/dist/guest/agents/local-state.js +13 -1
  27. package/dist/guest/boot/boot.js +4 -1
  28. package/dist/guest/setup/browser-gpu.d.ts +1 -0
  29. package/dist/guest/setup/browser-gpu.js +18 -0
  30. package/dist/guest/toolchains/native/rust-extras.js +3 -1
  31. package/dist/guest/toolchains/native/unreal.d.ts +9 -0
  32. package/dist/guest/toolchains/native/unreal.js +198 -0
  33. package/dist/guest/toolchains/native/vcruntime.d.ts +3 -0
  34. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  35. package/dist/guest/toolchains/plan.js +10 -2
  36. package/dist/host/engines/hcs/live-folders.js +1 -1
  37. package/dist/host/github/token.js +1 -1
  38. package/dist/host/profile/accounts.js +1 -1
  39. package/dist/host/profile/agent-accounts.js +1 -1
  40. package/dist/host/profile/plugin-share.d.ts +5 -0
  41. package/dist/host/profile/plugin-share.js +22 -1
  42. package/dist/host/secrets/secret-store.d.ts +3 -3
  43. package/dist/host/secrets/secret-store.js +37 -10
  44. package/dist/host/vpn/login.js +1 -1
  45. package/dist/runtime/agent.js +16 -1
  46. package/dist/runtime/daemon.js +531 -337
  47. package/dist/runtime/db.js +17 -1
  48. package/dist/runtime/main.js +694 -472
  49. package/dist/runtime/netproxy.js +16 -1
  50. package/dist/runtime/view.js +16 -1
  51. package/dist/selftest/accounts.test.js +7 -0
  52. package/dist/selftest/agent-paths.test.js +12 -1
  53. package/dist/selftest/detect.test.js +7 -0
  54. package/dist/selftest/live-network.test.js +5 -0
  55. package/dist/selftest/support/fixtures.d.ts +6 -0
  56. package/dist/shared/channel/schema.d.ts +18 -0
  57. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  58. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  59. package/native/csharp/HostTools/Program.cs +6 -1
  60. package/native/csharp/HostTools/Protect.cs +41 -0
  61. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  62. package/package.json +1 -1
  63. package/schemas/config.schema.json +34 -1
  64. package/schemas/sandbox-config.schema.json +34 -1
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-7EXDUNE7.js";
6
- import "./chunk-HIC4CUMY.js";
5
+ } from "./chunk-QLGGAG3D.js";
6
+ import "./chunk-REE3EMHD.js";
7
7
 
8
8
  // src/bin/sbx-show.ts
9
9
  run(process.argv.slice(2), { command: "show" });
@@ -95,7 +95,7 @@ function remoteAudit(ctx) {
95
95
  * served (it refuses everyone while web.remote.windowsSignIn is off).
96
96
  */
97
97
  function remoteGate(ctx) {
98
- const secrets = SecretStore.standard();
98
+ const secrets = SecretStore.standard(ctx.storageRoot);
99
99
  let ownerSid = '';
100
100
  try {
101
101
  ownerSid = currentUserSid();
@@ -62,6 +62,7 @@ export const DEFAULTS = {
62
62
  linkSlots: 'auto',
63
63
  },
64
64
  msvc: { enabled: 'auto', acceptLicense: false, msvcVersion: 'latest', sdkVersion: 'latest' },
65
+ unreal: { enabled: 'auto', acceptLicense: false, components: [], keepInstallFiles: true },
65
66
  cpp: { enabled: 'auto', flexBison: true },
66
67
  dotnet: {
67
68
  enabled: 'auto',
@@ -4,6 +4,7 @@ export declare const MARKERS: {
4
4
  readonly python: readonly ["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg", "Pipfile", "uv.lock", "poetry.lock", ".python-version", "*.py", "*.ipynb"];
5
5
  readonly rust: readonly ["Cargo.toml", "rust-toolchain", "rust-toolchain.toml"];
6
6
  readonly msvc: readonly ["*.vcxproj", "CMakePresets.json", "CMakeUserPresets.json"];
7
+ readonly unreal: readonly ["*.uproject", "*.uplugin"];
7
8
  readonly cpp: readonly ["CMakeLists.txt", "Makefile", "meson.build", "premake5.lua", "Cargo.toml", "*.vcxproj", "*.c", "*.cpp", "*.cc", "*.h", "*.hpp"];
8
9
  readonly dotnet: readonly ["*.sln", "*.slnx", "*.csproj", "*.fsproj", "*.vbproj", "global.json", "*.cs"];
9
10
  readonly go: readonly ["go.mod", "go.work", "*.go"];
@@ -15,6 +15,7 @@ export const MARKERS = {
15
15
  ],
16
16
  rust: ['Cargo.toml', 'rust-toolchain', 'rust-toolchain.toml'],
17
17
  msvc: ['*.vcxproj', 'CMakePresets.json', 'CMakeUserPresets.json'],
18
+ unreal: ['*.uproject', '*.uplugin'],
18
19
  cpp: [
19
20
  'CMakeLists.txt',
20
21
  'Makefile',
@@ -86,6 +87,12 @@ export function decideToolchains(settings, entries, pcHas, hints = {}) {
86
87
  on: false,
87
88
  reason: 'would be used (' + result.msvc.reason + '); set toolchains.msvc.acceptLicense to install it',
88
89
  };
90
+ // Unreal is a ~10 GB licensed install every boot: "auto" only installs it once its licence is accepted.
91
+ if (settings.unreal.enabled === 'auto' && result.unreal.on && !settings.unreal.acceptLicense)
92
+ result.unreal = {
93
+ on: false,
94
+ reason: 'would be used (' + result.unreal.reason + '); set toolchains.unreal.acceptLicense to install it',
95
+ };
89
96
  if (result.rust.on && !result.msvc.on && !result.cpp.on && settings.cpp.enabled !== false)
90
97
  result.cpp = { on: true, reason: 'the linker for Rust' };
91
98
  // uv installs the Python tools (`uv tool install`), so Python tools turn an "auto" uv on.
@@ -153,6 +153,12 @@ export declare const ConfigSchema: z.ZodObject<{
153
153
  msvcVersion: z.ZodString;
154
154
  sdkVersion: z.ZodString;
155
155
  }, z.core.$strict>;
156
+ unreal: z.ZodObject<{
157
+ enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
158
+ acceptLicense: z.ZodBoolean;
159
+ components: z.ZodArray<z.ZodString>;
160
+ keepInstallFiles: z.ZodBoolean;
161
+ }, z.core.$strict>;
156
162
  cpp: z.ZodObject<{
157
163
  enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
158
164
  flexBison: z.ZodBoolean;
@@ -506,6 +512,21 @@ export declare const LocalOverridesSchema: z.ZodObject<{
506
512
  zstd: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
507
513
  openssl: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
508
514
  }, z.core.$strict>>;
515
+ unreal: z.ZodOptional<z.ZodObject<{
516
+ enabled: z.ZodOptional<z.ZodBoolean>;
517
+ }, z.core.$strict> | z.ZodObject<{
518
+ version: z.ZodOptional<z.ZodString>;
519
+ extraVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
520
+ corepack: z.ZodOptional<z.ZodBoolean>;
521
+ volta: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
522
+ globalPackages: z.ZodOptional<z.ZodArray<z.ZodString>>;
523
+ }, z.core.$strict> | z.ZodObject<{
524
+ enabled: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
525
+ }, z.core.$strict> | z.ZodObject<{
526
+ gh: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
527
+ zstd: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
528
+ openssl: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
529
+ }, z.core.$strict>>;
509
530
  cpp: z.ZodOptional<z.ZodObject<{
510
531
  enabled: z.ZodOptional<z.ZodBoolean>;
511
532
  }, z.core.$strict> | z.ZodObject<{
@@ -51,7 +51,7 @@ export const SandboxSchema = z.strictObject({
51
51
  .describe('When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it.'),
52
52
  vGpu: z
53
53
  .boolean()
54
- .describe("Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it)."),
54
+ .describe("Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."),
55
55
  networking: z.boolean().describe('Internet access. false is the only hard network isolation Windows Sandbox offers.'),
56
56
  clipboard: z
57
57
  .boolean()
@@ -34,6 +34,12 @@ export declare const ToolchainsSchema: z.ZodObject<{
34
34
  msvcVersion: z.ZodString;
35
35
  sdkVersion: z.ZodString;
36
36
  }, z.core.$strict>;
37
+ unreal: z.ZodObject<{
38
+ enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
39
+ acceptLicense: z.ZodBoolean;
40
+ components: z.ZodArray<z.ZodString>;
41
+ keepInstallFiles: z.ZodBoolean;
42
+ }, z.core.$strict>;
37
43
  cpp: z.ZodObject<{
38
44
  enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
39
45
  flexBison: z.ZodBoolean;
@@ -53,6 +53,16 @@ export const ToolchainsSchema = z.strictObject({
53
53
  msvcVersion: z.string().min(1).describe('"latest" or an MSVC tools version prefix, e.g. "14.44".'),
54
54
  sdkVersion: z.string().min(1).describe('"latest" or a Windows SDK build, e.g. "26100" or "10.0.22621".'),
55
55
  }),
56
+ unreal: z.strictObject({
57
+ enabled: enabled('Unreal Engine development: Visual Studio Community 2022 with the C++ game workload (MSVC, a Windows 11 SDK, .NET Framework targeting packs, Unreal integration and the Epic Games Launcher) and the DirectX End-User Runtime. A real installer: it runs every boot (~10 GB on the sandbox disk). The engine itself comes from the Epic Games Launcher or its source.', 'a .uproject or .uplugin'),
58
+ acceptLicense: z
59
+ .boolean()
60
+ .describe('Required to install: you accept the Visual Studio Community licence terms (https://visualstudio.microsoft.com/license-terms/) and the DirectX End-User Runtime licence.'),
61
+ components: StringsSchema.describe('More Visual Studio workload or component IDs to add, e.g. ["Microsoft.VisualStudio.Component.VC.Llvm.Clang"].'),
62
+ keepInstallFiles: z
63
+ .boolean()
64
+ .describe('Keep an offline copy of the Visual Studio install files in tools\\visualstudio (~10 GB, made once), so later boots install without downloading them again. Skipped while that disk has too little room.'),
65
+ }),
56
66
  cpp: z.strictObject({
57
67
  enabled: enabled("MinGW-w64 GCC with CMake, Ninja, premake5 and mingw32-make (also Rust's linker).", 'CMakeLists.txt, Makefile, meson.build, premake5.lua or Cargo.toml'),
58
68
  flexBison: z
@@ -48,6 +48,12 @@ export declare function bootResetPaths(agents: Readonly<Record<string, Harness>>
48
48
  agent: string;
49
49
  path: string;
50
50
  }[];
51
+ /**
52
+ * Codex's config.toml with its own command sandbox off, or null when it already says which one to use. Inside Windows
53
+ * Sandbox that inner sandbox only costs: its setup re-grants ACLs over the whole profile at every boot (minutes), and
54
+ * every command hangs until it is done. A `sandbox_mode` the user set (before the first [table]) is kept.
55
+ */
56
+ export declare function codexConfigWithoutSandbox(toml: string): string | null;
51
57
  /** A SQLite database file or one of its -wal / -shm siblings. */
52
58
  export declare const isDatabaseFile: (name: string) => boolean;
53
59
  /** Files that make a consistent copy: the databases and their -wal (the -shm is rebuilt by SQLite). */
@@ -61,6 +61,20 @@ export function localStateEntries(agents, stateDir, localRoot) {
61
61
  export function bootResetPaths(agents, stateDir) {
62
62
  return Object.entries(agents).flatMap(([agent, harness]) => (harness.bootReset ?? []).map((rel) => ({ agent, path: path.join(stateDir, agent, rel) })));
63
63
  }
64
+ /**
65
+ * Codex's config.toml with its own command sandbox off, or null when it already says which one to use. Inside Windows
66
+ * Sandbox that inner sandbox only costs: its setup re-grants ACLs over the whole profile at every boot (minutes), and
67
+ * every command hangs until it is done. A `sandbox_mode` the user set (before the first [table]) is kept.
68
+ */
69
+ export function codexConfigWithoutSandbox(toml) {
70
+ const firstTable = toml.search(/^\s*\[/m);
71
+ const topLevel = firstTable < 0 ? toml : toml.slice(0, firstTable);
72
+ if (/^\s*sandbox_mode\s*=/m.test(topLevel))
73
+ return null;
74
+ return ('# Set by the sandbox: Windows Sandbox already contains Codex, its own command sandbox would only slow it down.\n' +
75
+ 'sandbox_mode = "danger-full-access"\n' +
76
+ (toml ? '\n' + toml : ''));
77
+ }
64
78
  /** A SQLite database file or one of its -wal / -shm siblings. */
65
79
  export const isDatabaseFile = (name) => /\.(sqlite3?|db)(-wal|-shm)?$/i.test(name);
66
80
  /** Files that make a consistent copy: the databases and their -wal (the -shm is rebuilt by SQLite). */
@@ -5,6 +5,8 @@ export declare function agentLocalStates(): LocalState[];
5
5
  export declare function localStateVars(): Record<string, string>;
6
6
  /** Boot: what an agent sealed to the last sandbox (bootReset) is deleted, so it sets it up again. Links are left. */
7
7
  export declare function resetMachineBound(): void;
8
+ /** Boot: Codex runs its commands without its own sandbox (codexConfigWithoutSandbox), unless the user picked one. */
9
+ export declare function codexWithoutInnerSandbox(): void;
8
10
  /** Boot: the saved databases of every configured agent are copied to the sandbox's disk, where the agent runs them. */
9
11
  export declare function restoreLocalState(): void;
10
12
  /**
@@ -10,7 +10,7 @@ import { execOutput } from '../runtime/process.js';
10
10
  import { log, warn } from '../runtime/log.js';
11
11
  import { session, stateDir } from '../runtime/session.js';
12
12
  import { errMsg } from '../../core/util/errors.js';
13
- import { bootResetPaths, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, } from './agent-files.js';
13
+ import { bootResetPaths, codexConfigWithoutSandbox, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, } from './agent-files.js';
14
14
  const SQLITE_HEADER = 'SQLite format 3\0';
15
15
  /** The sandbox-disk folder that holds the working copies. */
16
16
  const localRoot = () => path.join(localDir, 'agent-state');
@@ -102,6 +102,18 @@ export function resetMachineBound() {
102
102
  }
103
103
  }
104
104
  }
105
+ /** Boot: Codex runs its commands without its own sandbox (codexConfigWithoutSandbox), unless the user picked one. */
106
+ export function codexWithoutInnerSandbox() {
107
+ if (dryRun || !session.agents?.codex)
108
+ return;
109
+ const file = path.join(stateDir, 'codex', 'config.toml');
110
+ const updated = codexConfigWithoutSandbox(exists(file) ? fs.readFileSync(file, 'utf8') : '');
111
+ if (updated === null)
112
+ return;
113
+ mkdirp(path.dirname(file));
114
+ fs.writeFileSync(file, updated, 'utf8');
115
+ log('codex: its own command sandbox is off (sandbox_mode = "danger-full-access" in config.toml).');
116
+ }
105
117
  /** Boot: the saved databases of every configured agent are copied to the sandbox's disk, where the agent runs them. */
106
118
  export function restoreLocalState() {
107
119
  if (dryRun)
@@ -6,7 +6,7 @@ import { writeGuides } from './guide.js';
6
6
  import { setupToolchains } from '../toolchains/index.js';
7
7
  import { agents } from '../agents/agent-installer.js';
8
8
  import { agentShims, sbxAgentText, shimText } from '../agents/agent-files.js';
9
- import { resetMachineBound, restoreLocalState } from '../agents/local-state.js';
9
+ import { codexWithoutInnerSandbox, resetMachineBound, restoreLocalState } from '../agents/local-state.js';
10
10
  import { installShellPath } from '../setup/shell-path.js';
11
11
  import { linkProfileDirs } from '../agents/profile-links.js';
12
12
  import { restoreClaudeMem } from '../setup/claude-mem.js';
@@ -19,6 +19,7 @@ import { writeManagedSettings } from '../setup/managed-policy.js';
19
19
  import { writeAddonMcpServers } from '../setup/addon-mcp.js';
20
20
  import { startHeadroom } from '../setup/headroom.js';
21
21
  import { applyPersonalization } from '../setup/desktop.js';
22
+ import { keepBrowsersOffGpu } from '../setup/browser-gpu.js';
22
23
  import { setupGitConfig } from '../setup/git-identity.js';
23
24
  import { loginGithub, prepareGithubFolder } from '../setup/github-token.js';
24
25
  import { ensurePtyHost } from '../sessions/pty-host.js';
@@ -172,6 +173,7 @@ export class Boot {
172
173
  step(2, 'Pre-provision hooks and desktop');
173
174
  runHooks('pre-provision', process.env);
174
175
  await soft('Personalisation', applyPersonalization);
176
+ await soft('Browsers without the GPU', keepBrowsersOffGpu);
175
177
  }
176
178
  /** Toolchains, databases, apps, the network proxy and VS Code; then the agent environment, saved machine-wide. */
177
179
  async developerTools() {
@@ -247,6 +249,7 @@ export class Boot {
247
249
  await soft('Agent databases restore', restoreLocalState);
248
250
  // Agent add-ons: MCP servers in the sandbox's Claude Code, and the Headroom proxy before the managed settings point
249
251
  // Claude Code at it (the guide reads them back, so it comes last).
252
+ await soft('Codex without its own sandbox', codexWithoutInnerSandbox);
250
253
  let headroom = null;
251
254
  await soft('MCP servers from Apps', writeAddonMcpServers);
252
255
  await soft('Headroom proxy', async () => {
@@ -0,0 +1 @@
1
+ export declare function keepBrowsersOffGpu(): void;
@@ -0,0 +1,18 @@
1
+ // Browsers in the sandbox draw without the GPU (Edge and Chrome machine policy, written every boot). With sandbox.vGpu
2
+ // the sandbox shares this PC's graphics card through one virtual GPU: a browser's GPU process starting next to a game
3
+ // or other Direct3D program there has taken the whole sandbox down. Pages still render, WebGL included, in software;
4
+ // programs that need the GPU keep it. Without vGPU nothing changes (there is no GPU to use).
5
+ import { SYSTEM } from '../../shared/system.js';
6
+ import { dryRun, isWin } from '../runtime/paths.js';
7
+ import { exec } from '../runtime/process.js';
8
+ import { log } from '../runtime/log.js';
9
+ const POLICY_KEYS = ['HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge', 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome'];
10
+ export function keepBrowsersOffGpu() {
11
+ if (dryRun || !isWin)
12
+ return;
13
+ for (const key of POLICY_KEYS)
14
+ exec(SYSTEM.reg(), ['add', key, '/v', 'HardwareAccelerationModeEnabled', '/t', 'REG_DWORD', '/d', '0', '/f'], {
15
+ stdio: 'ignore',
16
+ });
17
+ log('Browsers draw without the GPU (Edge and Chrome policy).');
18
+ }
@@ -98,5 +98,7 @@ export function rustHost(config) {
98
98
  if (config.rust.host !== 'auto')
99
99
  return config.rust.host;
100
100
  const cpu = arch === 'arm64' ? 'aarch64' : 'x86_64';
101
- return cpu + (wanted(config.msvc.enabled, 'msvc') ? '-pc-windows-msvc' : '-pc-windows-gnu');
101
+ // Unreal's Visual Studio is a full MSVC too (rustc finds it through the Setup API).
102
+ const msvc = wanted(config.msvc.enabled, 'msvc') || wanted(config.unreal.enabled, 'unreal');
103
+ return cpu + (msvc ? '-pc-windows-msvc' : '-pc-windows-gnu');
102
104
  }
@@ -0,0 +1,9 @@
1
+ import type { Toolchains } from '../../../core/types.js';
2
+ import { type ToolEnv } from '../tool-env.js';
3
+ /**
4
+ * What Epic lists for Unreal Engine 5 with Visual Studio 2022: the game workload, MSVC v143, a Windows SDK of
5
+ * 10.0.18362 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
6
+ * Visual Studio's Unreal support and the Unreal Engine installer (the Epic Games Launcher).
7
+ */
8
+ export declare const UNREAL_COMPONENTS: readonly ["Microsoft.VisualStudio.Workload.NativeGame", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "Microsoft.VisualStudio.Component.Windows11SDK.22621", "Microsoft.VisualStudio.Component.VC.DiagnosticTools", "Microsoft.Net.Component.4.6.2.TargetingPack", "Microsoft.Net.Component.4.8.SDK", "Component.Unreal.Ide", "Component.Unreal"];
9
+ export declare function setupUnreal(env: ToolEnv, config: Toolchains['unreal']): Promise<void>;
@@ -0,0 +1,198 @@
1
+ // Unreal Engine development (toolchains.unreal): Visual Studio Community 2022 with the C++ game workload and the parts
2
+ // Unreal's build tool looks for (it finds Visual Studio through the Setup API, so the portable MSVC does not do), and
3
+ // the DirectX End-User Runtime. Real installers: they run every boot. The installers are kept in tools\ and run only
4
+ // when Microsoft's signature on them checks out; with keepInstallFiles an offline copy of the install files
5
+ // (tools\visualstudio\layout) spares later boots the download. The engine itself comes from the Epic Games Launcher.
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { z } from 'zod';
10
+ import { spawnAsync } from '../../../shared/download/index.js';
11
+ import { writeJsonAtomic } from '../../../shared/io/json.js';
12
+ import { safeRemove } from '../../../shared/io/safe-remove.js';
13
+ import { dryRun } from '../../runtime/paths.js';
14
+ import { execOutput } from '../../runtime/process.js';
15
+ import { exists, mkdirp } from '../../runtime/fs.js';
16
+ import { log, warn } from '../../runtime/log.js';
17
+ import { errMsg } from '../../../core/util/errors.js';
18
+ import { downloadFile } from '../install.js';
19
+ import { tool } from '../tool-env.js';
20
+ import { MICROSOFT_SIGNER, signerOf } from './vcruntime.js';
21
+ const BOOTSTRAPPER_URL = 'https://aka.ms/vs/17/release/vs_community.exe';
22
+ const DIRECTX_URL = 'https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe';
23
+ /**
24
+ * What Epic lists for Unreal Engine 5 with Visual Studio 2022: the game workload, MSVC v143, a Windows SDK of
25
+ * 10.0.18362 or newer, the C++ profiling tools, the .NET Framework targeting packs some engine tools build against,
26
+ * Visual Studio's Unreal support and the Unreal Engine installer (the Epic Games Launcher).
27
+ */
28
+ export const UNREAL_COMPONENTS = [
29
+ 'Microsoft.VisualStudio.Workload.NativeGame',
30
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
31
+ 'Microsoft.VisualStudio.Component.Windows11SDK.22621',
32
+ 'Microsoft.VisualStudio.Component.VC.DiagnosticTools',
33
+ 'Microsoft.Net.Component.4.6.2.TargetingPack',
34
+ 'Microsoft.Net.Component.4.8.SDK',
35
+ 'Component.Unreal.Ide',
36
+ 'Component.Unreal',
37
+ ];
38
+ /** Visual Studio installer exit codes that mean installed: 0, and 3010 (a restart would finish it, not needed here). */
39
+ const OK_EXIT_CODES = [0, 3010];
40
+ /** Free room the offline copy needs before it is made (it grows to about 10 GB). */
41
+ const LAYOUT_ROOM_BYTES = 15 * 1024 ** 3;
42
+ /**
43
+ * Free room on the sandbox's own disk before Visual Studio goes on it: it takes about 7 GB, and the disk (about 40 GB)
44
+ * also holds the paging file and every build, so it is installed only when about 13 GB stay free after it.
45
+ */
46
+ const INSTALL_ROOM_BYTES = 20 * 1024 ** 3;
47
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
48
+ const VS_DIR = tool('visualstudio');
49
+ const BOOTSTRAPPER = path.join(VS_DIR, 'vs_community.exe');
50
+ const LAYOUT = path.join(VS_DIR, 'layout');
51
+ const LAYOUT_MARKER = path.join(LAYOUT, '.complete.json');
52
+ const LayoutSchema = z.object({ components: z.array(z.string()) });
53
+ const VSWHERE = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
54
+ /** The newest Visual Studio with the C++ compiler: its folder and version, or null. */
55
+ function installedVs() {
56
+ if (!exists(VSWHERE))
57
+ return null;
58
+ const ask = (property) => execOutput(VSWHERE, [
59
+ '-latest',
60
+ '-products',
61
+ '*',
62
+ '-requires',
63
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
64
+ '-property',
65
+ property,
66
+ ]);
67
+ const at = ask('installationPath');
68
+ return at ? { path: at, version: ask('installationVersion') || 'installed' } : null;
69
+ }
70
+ /** A Microsoft-signed installer kept in tools\ (downloaded once); a file with a bad signature is deleted. */
71
+ async function signedInstaller(url, file, what) {
72
+ if (!exists(file)) {
73
+ log('Downloading ' + what + ' ...');
74
+ mkdirp(path.dirname(file));
75
+ await downloadFile(url, file);
76
+ }
77
+ if (signerOf(file) !== MICROSOFT_SIGNER) {
78
+ fs.rmSync(file, { force: true });
79
+ throw new Error(path.basename(file) + ' is not signed by Microsoft; deleted it (it is downloaded again next boot)');
80
+ }
81
+ return file;
82
+ }
83
+ function freeBytes(dir) {
84
+ try {
85
+ const s = fs.statfsSync(dir);
86
+ return s.bavail * s.bsize;
87
+ }
88
+ catch {
89
+ return 0;
90
+ }
91
+ }
92
+ /** The offline copy's installer when it holds every component; made or completed when there is room. */
93
+ async function layoutInstaller(bootstrapper, ids) {
94
+ const installer = path.join(LAYOUT, 'vs_community.exe');
95
+ const has = () => {
96
+ if (!exists(LAYOUT_MARKER) || !exists(installer))
97
+ return false;
98
+ try {
99
+ const done = LayoutSchema.parse(JSON.parse(fs.readFileSync(LAYOUT_MARKER, 'utf8')));
100
+ return ids.every((id) => done.components.includes(id));
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ };
106
+ if (has())
107
+ return installer;
108
+ if (freeBytes(mkdirp(VS_DIR)) < LAYOUT_ROOM_BYTES) {
109
+ warn('unreal: not keeping the install files (less than 15 GB free in ' + VS_DIR + '); downloading this boot');
110
+ return null;
111
+ }
112
+ log('Saving the Visual Studio install files for later boots (~10 GB, once) ...');
113
+ fs.rmSync(LAYOUT_MARKER, { force: true });
114
+ const code = await spawnAsync(bootstrapper, [
115
+ '--layout',
116
+ LAYOUT,
117
+ '--lang',
118
+ 'en-US',
119
+ '--quiet',
120
+ '--wait',
121
+ ...ids.flatMap((id) => ['--add', id]),
122
+ ]);
123
+ if (!OK_EXIT_CODES.includes(code) || !exists(installer)) {
124
+ warn('unreal: saving the install files failed (exit ' + code + '); downloading this boot');
125
+ return null;
126
+ }
127
+ writeJsonAtomic(LAYOUT_MARKER, { components: [...ids] });
128
+ return installer;
129
+ }
130
+ async function installVisualStudio(config, ids) {
131
+ const bootstrapper = await signedInstaller(BOOTSTRAPPER_URL, BOOTSTRAPPER, 'the Visual Studio installer');
132
+ let installer = null;
133
+ if (config.keepInstallFiles) {
134
+ try {
135
+ installer = await layoutInstaller(bootstrapper, ids);
136
+ }
137
+ catch (e) {
138
+ warn('unreal: the offline copy failed (' + errMsg(e) + '); downloading this boot');
139
+ }
140
+ }
141
+ log('Installing Visual Studio Community 2022 with the C++ game workload' +
142
+ (installer ? ' from the saved install files' : ' (downloads ~5-8 GB)') +
143
+ ' ...');
144
+ const code = await spawnAsync(installer ?? bootstrapper, [
145
+ '--quiet',
146
+ '--wait',
147
+ '--norestart',
148
+ '--nocache',
149
+ ...(installer ? ['--noWeb'] : []),
150
+ ...ids.flatMap((id) => ['--add', id]),
151
+ ]);
152
+ if (!OK_EXIT_CODES.includes(code) || !installedVs())
153
+ throw new Error('the Visual Studio installer failed (exit ' + code + '); its logs are the dd_*.log files in ' + os.tmpdir());
154
+ }
155
+ /** The June 2010 DirectX End-User Runtime (D3DX, XAudio 2.7, XInput 1.3), which Epic lists for the engine. */
156
+ async function installDirectX() {
157
+ const system32 = path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32');
158
+ if (exists(path.join(system32, 'd3dx9_43.dll')) && exists(path.join(system32, 'XAudio2_7.dll')))
159
+ return;
160
+ const redist = await signedInstaller(DIRECTX_URL, path.join(VS_DIR, 'directx_Jun2010_redist.exe'), 'the DirectX End-User Runtime (~95 MB once)');
161
+ const unpacked = path.join(os.tmpdir(), 'directx-redist');
162
+ safeRemove(unpacked, os.tmpdir());
163
+ mkdirp(unpacked);
164
+ if ((await spawnAsync(redist, ['/Q', '/T:' + unpacked])) !== 0 || !exists(path.join(unpacked, 'DXSETUP.exe')))
165
+ throw new Error('could not unpack the DirectX End-User Runtime');
166
+ const code = await spawnAsync(path.join(unpacked, 'DXSETUP.exe'), ['/silent']);
167
+ safeRemove(unpacked, os.tmpdir());
168
+ if (code !== 0)
169
+ throw new Error('the DirectX End-User Runtime installer failed (exit ' + code + ')');
170
+ }
171
+ export async function setupUnreal(env, config) {
172
+ if (!config.acceptLicense)
173
+ throw new Error('Unreal tools are installed only with toolchains.unreal.acceptLicense = true: Visual Studio Community and the DirectX End-User Runtime come under Microsoft licence terms (https://visualstudio.microsoft.com/license-terms/)');
174
+ const bad = config.components.find((id) => !ID.test(id));
175
+ if (bad)
176
+ throw new Error('toolchains.unreal.components: "' + bad + '" is not a Visual Studio component ID');
177
+ const ids = [...new Set([...UNREAL_COMPONENTS, ...config.components])];
178
+ if (dryRun) {
179
+ log('(dry run) would install Visual Studio Community 2022 with ' + ids.join(', ') + ' and DirectX');
180
+ return;
181
+ }
182
+ if (!installedVs()) {
183
+ const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
184
+ const free = freeBytes(programFiles);
185
+ if (free < INSTALL_ROOM_BYTES)
186
+ throw new Error('not installing Visual Studio: the sandbox disk has ' +
187
+ (free / 1024 ** 3).toFixed(1) +
188
+ ' GB free and it needs 20 GB (7 GB for Visual Studio, the rest for builds and the paging file)');
189
+ await installVisualStudio(config, ids);
190
+ }
191
+ await installDirectX();
192
+ const vs = installedVs();
193
+ env.summary.push('unreal: Visual Studio Community ' +
194
+ vs.version +
195
+ ' (C++ game workload, Windows 11 SDK, Unreal support) at ' +
196
+ vs.path +
197
+ '; DirectX End-User Runtime; get the engine from the Epic Games Launcher (Start menu)');
198
+ }
@@ -1,4 +1,7 @@
1
1
  import { type ToolEnv } from '../tool-env.js';
2
+ export declare const MICROSOFT_SIGNER = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US";
3
+ /** The subject of a valid Authenticode signature, or '' when the file is unsigned or tampered with. */
4
+ export declare const signerOf: (file: string) => string;
2
5
  /**
3
6
  * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
4
7
  * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
@@ -9,11 +9,11 @@ import { log } from '../../runtime/log.js';
9
9
  import { downloadFile } from '../install.js';
10
10
  import { tool } from '../tool-env.js';
11
11
  const VC_REDIST = 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
12
- const MICROSOFT_SIGNER = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US';
12
+ export const MICROSOFT_SIGNER = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US';
13
13
  /** 1638: a newer version is already installed; 3010: installed, a restart would finish it (not needed for the DLLs). */
14
14
  const OK_EXIT_CODES = [0, 1638, 3010];
15
15
  /** The subject of a valid Authenticode signature, or '' when the file is unsigned or tampered with. */
16
- const signerOf = (file) => scriptOutput('get-signer.ps1', ['-Path', file]).trim();
16
+ export const signerOf = (file) => scriptOutput('get-signer.ps1', ['-Path', file]).trim();
17
17
  /**
18
18
  * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
19
19
  * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
@@ -11,6 +11,7 @@ import { setupCpp } from './native/cpp.js';
11
11
  import { setupDotnet } from './native/dotnet.js';
12
12
  import { setupMesa } from './native/mesa.js';
13
13
  import { setupMsvc } from './native/msvc/index.js';
14
+ import { setupUnreal } from './native/unreal.js';
14
15
  import { setupRust } from './native/rust.js';
15
16
  import { setupNode } from './languages/node.js';
16
17
  import { setupPwsh } from './tools/pwsh.js';
@@ -26,6 +27,7 @@ export function plan(config, versions, o) {
26
27
  const apps = Boolean(o.apps && (o.apps.install.length || Object.keys(o.apps.custom).length));
27
28
  const java = wanted(config.java.enabled, 'java');
28
29
  const python = wanted(config.python.enabled, 'python');
30
+ const unreal = wanted(config.unreal.enabled, 'unreal');
29
31
  return [
30
32
  {
31
33
  name: 'Git',
@@ -43,12 +45,18 @@ export function plan(config, versions, o) {
43
45
  enabled: wanted(config.cpp.enabled, 'cpp') || wanted(config.rust.enabled, 'rust'),
44
46
  install: (env) => setupCpp(env, config.cpp),
45
47
  },
46
- { name: 'MSVC', enabled: wanted(config.msvc.enabled, 'msvc'), install: (env) => setupMsvc(env, config.msvc) },
48
+ // Unreal's Visual Studio brings MSVC and the real vswhere: the portable MSVC (and its stand-in vswhere) stays out.
49
+ { name: 'Unreal', enabled: unreal, shared: true, install: (env) => setupUnreal(env, config.unreal) },
50
+ {
51
+ name: 'MSVC',
52
+ enabled: wanted(config.msvc.enabled, 'msvc') && !unreal,
53
+ install: (env) => setupMsvc(env, config.msvc),
54
+ },
47
55
  // Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
48
56
  {
49
57
  name: 'Rust',
50
58
  enabled: wanted(config.rust.enabled, 'rust'),
51
- after: ['C/C++', 'MSVC'],
59
+ after: ['C/C++', 'MSVC', 'Unreal'],
52
60
  install: async (env) => {
53
61
  await setupRust(env, { ...config.rust, host: rustHost(config) }, o.refresh);
54
62
  Object.assign(env.vars, rustCaches(config.rust));
@@ -331,7 +331,7 @@ export function realLiveFolders(storageRoot, dataPath) {
331
331
  return new LiveFolders({
332
332
  storageRoot,
333
333
  dataPath,
334
- secrets: SecretStore.standard(),
334
+ secrets: SecretStore.standard(storageRoot),
335
335
  elevated: new ScriptLivePort(),
336
336
  userSid: currentUserSid,
337
337
  isAlive: isSandboxRunning,
@@ -23,7 +23,7 @@ export function checkGithubToken(token) {
23
23
  export class GithubToken {
24
24
  storageRoot;
25
25
  secrets;
26
- constructor(storageRoot, secrets = SecretStore.standard()) {
26
+ constructor(storageRoot, secrets = SecretStore.standard(storageRoot)) {
27
27
  this.storageRoot = storageRoot;
28
28
  this.secrets = secrets;
29
29
  }
@@ -69,7 +69,7 @@ export class AccountStore {
69
69
  /** What the last activate() did with the shared plugins (a problem there never stops the switch). */
70
70
  pluginNote = null;
71
71
  /** @param sharePlugins claudeProfile.sharePluginsAcrossAccounts: carry Claude plugins to the account switched to */
72
- constructor(storageRoot, sharePlugins = false, secrets = SecretStore.standard()) {
72
+ constructor(storageRoot, sharePlugins = false, secrets = SecretStore.standard(storageRoot)) {
73
73
  this.storageRoot = storageRoot;
74
74
  this.sharePlugins = sharePlugins;
75
75
  this.secrets = secrets;
@@ -34,7 +34,7 @@ export class AgentAccountStore {
34
34
  /**
35
35
  * @param loginFiles files of the login, relative to the folder they are read from and written to
36
36
  */
37
- constructor(storageRoot, agent, loginFiles, secrets = SecretStore.standard()) {
37
+ constructor(storageRoot, agent, loginFiles, secrets = SecretStore.standard(storageRoot)) {
38
38
  this.storageRoot = storageRoot;
39
39
  this.agent = agent;
40
40
  this.loginFiles = loginFiles;
@@ -21,6 +21,11 @@ export declare class PluginShare {
21
21
  save(dataPath: string, claudeDir: string): void;
22
22
  /** The shared set goes into a data set's Claude folder; its own settings win, its other settings are untouched. */
23
23
  restore(dataPath: string, claudeDir: string): void;
24
+ /**
25
+ * Whether the shared set already holds this Claude folder's plugins: the same plugin records and plugin settings.
26
+ * Copying a big plugins folder takes seconds, so a switch between accounts with the same plugins skips it.
27
+ */
28
+ matches(claudeDir: string): boolean;
24
29
  /** One step when an account is switched to. */
25
30
  sync(dataPath: string, claudeDir: string): PluginShareResult;
26
31
  }