windows-development-sandbox 1.16.0 → 1.17.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.
- package/config.example.json +6 -0
- package/dist/app/features/access/access.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-7EXDUNE7.js → chunk-QLGGAG3D.js} +1 -1
- package/dist/bin/{chunk-HIC4CUMY.js → chunk-REE3EMHD.js} +73 -16
- package/dist/bin/{chunk-PXTLKIV4.js → chunk-XYUVIVJT.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/web.js +1 -1
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/detect/toolchains.d.ts +1 -0
- package/dist/core/detect/toolchains.js +7 -0
- package/dist/core/schema/config.d.ts +21 -0
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/core/schema/toolchains.d.ts +6 -0
- package/dist/core/schema/toolchains.js +10 -0
- package/dist/guest/boot/boot.js +2 -0
- package/dist/guest/setup/browser-gpu.d.ts +1 -0
- package/dist/guest/setup/browser-gpu.js +18 -0
- package/dist/guest/toolchains/native/rust-extras.js +3 -1
- package/dist/guest/toolchains/native/unreal.d.ts +9 -0
- package/dist/guest/toolchains/native/unreal.js +186 -0
- package/dist/guest/toolchains/native/vcruntime.d.ts +3 -0
- package/dist/guest/toolchains/native/vcruntime.js +2 -2
- package/dist/guest/toolchains/plan.js +10 -2
- package/dist/host/engines/hcs/live-folders.js +1 -1
- package/dist/host/github/token.js +1 -1
- package/dist/host/profile/accounts.js +1 -1
- package/dist/host/profile/agent-accounts.js +1 -1
- package/dist/host/profile/plugin-share.d.ts +5 -0
- package/dist/host/profile/plugin-share.js +22 -1
- package/dist/host/secrets/secret-store.d.ts +3 -3
- package/dist/host/secrets/secret-store.js +37 -10
- package/dist/host/vpn/login.js +1 -1
- package/dist/runtime/agent.js +16 -1
- package/dist/runtime/daemon.js +522 -337
- package/dist/runtime/db.js +17 -1
- package/dist/runtime/main.js +669 -472
- package/dist/runtime/netproxy.js +16 -1
- package/dist/runtime/view.js +16 -1
- package/dist/selftest/accounts.test.js +7 -0
- package/dist/selftest/detect.test.js +7 -0
- package/dist/selftest/live-network.test.js +5 -0
- package/dist/selftest/support/fixtures.d.ts +6 -0
- package/dist/shared/channel/schema.d.ts +18 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +6 -1
- package/native/csharp/HostTools/Protect.cs +41 -0
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/schemas/config.schema.json +34 -1
- package/schemas/sandbox-config.schema.json +34 -1
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
43
|
+
const VS_DIR = tool('visualstudio');
|
|
44
|
+
const BOOTSTRAPPER = path.join(VS_DIR, 'vs_community.exe');
|
|
45
|
+
const LAYOUT = path.join(VS_DIR, 'layout');
|
|
46
|
+
const LAYOUT_MARKER = path.join(LAYOUT, '.complete.json');
|
|
47
|
+
const LayoutSchema = z.object({ components: z.array(z.string()) });
|
|
48
|
+
const VSWHERE = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
|
|
49
|
+
/** The newest Visual Studio with the C++ compiler: its folder and version, or null. */
|
|
50
|
+
function installedVs() {
|
|
51
|
+
if (!exists(VSWHERE))
|
|
52
|
+
return null;
|
|
53
|
+
const ask = (property) => execOutput(VSWHERE, [
|
|
54
|
+
'-latest',
|
|
55
|
+
'-products',
|
|
56
|
+
'*',
|
|
57
|
+
'-requires',
|
|
58
|
+
'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
|
|
59
|
+
'-property',
|
|
60
|
+
property,
|
|
61
|
+
]);
|
|
62
|
+
const at = ask('installationPath');
|
|
63
|
+
return at ? { path: at, version: ask('installationVersion') || 'installed' } : null;
|
|
64
|
+
}
|
|
65
|
+
/** A Microsoft-signed installer kept in tools\ (downloaded once); a file with a bad signature is deleted. */
|
|
66
|
+
async function signedInstaller(url, file, what) {
|
|
67
|
+
if (!exists(file)) {
|
|
68
|
+
log('Downloading ' + what + ' ...');
|
|
69
|
+
mkdirp(path.dirname(file));
|
|
70
|
+
await downloadFile(url, file);
|
|
71
|
+
}
|
|
72
|
+
if (signerOf(file) !== MICROSOFT_SIGNER) {
|
|
73
|
+
fs.rmSync(file, { force: true });
|
|
74
|
+
throw new Error(path.basename(file) + ' is not signed by Microsoft; deleted it (it is downloaded again next boot)');
|
|
75
|
+
}
|
|
76
|
+
return file;
|
|
77
|
+
}
|
|
78
|
+
function freeBytes(dir) {
|
|
79
|
+
try {
|
|
80
|
+
const s = fs.statfsSync(dir);
|
|
81
|
+
return s.bavail * s.bsize;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The offline copy's installer when it holds every component; made or completed when there is room. */
|
|
88
|
+
async function layoutInstaller(bootstrapper, ids) {
|
|
89
|
+
const installer = path.join(LAYOUT, 'vs_community.exe');
|
|
90
|
+
const has = () => {
|
|
91
|
+
if (!exists(LAYOUT_MARKER) || !exists(installer))
|
|
92
|
+
return false;
|
|
93
|
+
try {
|
|
94
|
+
const done = LayoutSchema.parse(JSON.parse(fs.readFileSync(LAYOUT_MARKER, 'utf8')));
|
|
95
|
+
return ids.every((id) => done.components.includes(id));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
if (has())
|
|
102
|
+
return installer;
|
|
103
|
+
if (freeBytes(mkdirp(VS_DIR)) < LAYOUT_ROOM_BYTES) {
|
|
104
|
+
warn('unreal: not keeping the install files (less than 15 GB free in ' + VS_DIR + '); downloading this boot');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
log('Saving the Visual Studio install files for later boots (~10 GB, once) ...');
|
|
108
|
+
fs.rmSync(LAYOUT_MARKER, { force: true });
|
|
109
|
+
const code = await spawnAsync(bootstrapper, [
|
|
110
|
+
'--layout',
|
|
111
|
+
LAYOUT,
|
|
112
|
+
'--lang',
|
|
113
|
+
'en-US',
|
|
114
|
+
'--quiet',
|
|
115
|
+
'--wait',
|
|
116
|
+
...ids.flatMap((id) => ['--add', id]),
|
|
117
|
+
]);
|
|
118
|
+
if (!OK_EXIT_CODES.includes(code) || !exists(installer)) {
|
|
119
|
+
warn('unreal: saving the install files failed (exit ' + code + '); downloading this boot');
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
writeJsonAtomic(LAYOUT_MARKER, { components: [...ids] });
|
|
123
|
+
return installer;
|
|
124
|
+
}
|
|
125
|
+
async function installVisualStudio(config, ids) {
|
|
126
|
+
const bootstrapper = await signedInstaller(BOOTSTRAPPER_URL, BOOTSTRAPPER, 'the Visual Studio installer');
|
|
127
|
+
let installer = null;
|
|
128
|
+
if (config.keepInstallFiles) {
|
|
129
|
+
try {
|
|
130
|
+
installer = await layoutInstaller(bootstrapper, ids);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
warn('unreal: the offline copy failed (' + errMsg(e) + '); downloading this boot');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
log('Installing Visual Studio Community 2022 with the C++ game workload' +
|
|
137
|
+
(installer ? ' from the saved install files' : ' (downloads ~5-8 GB)') +
|
|
138
|
+
' ...');
|
|
139
|
+
const code = await spawnAsync(installer ?? bootstrapper, [
|
|
140
|
+
'--quiet',
|
|
141
|
+
'--wait',
|
|
142
|
+
'--norestart',
|
|
143
|
+
'--nocache',
|
|
144
|
+
...(installer ? ['--noWeb'] : []),
|
|
145
|
+
...ids.flatMap((id) => ['--add', id]),
|
|
146
|
+
]);
|
|
147
|
+
if (!OK_EXIT_CODES.includes(code) || !installedVs())
|
|
148
|
+
throw new Error('the Visual Studio installer failed (exit ' + code + '); its logs are the dd_*.log files in ' + os.tmpdir());
|
|
149
|
+
}
|
|
150
|
+
/** The June 2010 DirectX End-User Runtime (D3DX, XAudio 2.7, XInput 1.3), which Epic lists for the engine. */
|
|
151
|
+
async function installDirectX() {
|
|
152
|
+
const system32 = path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32');
|
|
153
|
+
if (exists(path.join(system32, 'd3dx9_43.dll')) && exists(path.join(system32, 'XAudio2_7.dll')))
|
|
154
|
+
return;
|
|
155
|
+
const redist = await signedInstaller(DIRECTX_URL, path.join(VS_DIR, 'directx_Jun2010_redist.exe'), 'the DirectX End-User Runtime (~95 MB once)');
|
|
156
|
+
const unpacked = path.join(os.tmpdir(), 'directx-redist');
|
|
157
|
+
safeRemove(unpacked, os.tmpdir());
|
|
158
|
+
mkdirp(unpacked);
|
|
159
|
+
if ((await spawnAsync(redist, ['/Q', '/T:' + unpacked])) !== 0 || !exists(path.join(unpacked, 'DXSETUP.exe')))
|
|
160
|
+
throw new Error('could not unpack the DirectX End-User Runtime');
|
|
161
|
+
const code = await spawnAsync(path.join(unpacked, 'DXSETUP.exe'), ['/silent']);
|
|
162
|
+
safeRemove(unpacked, os.tmpdir());
|
|
163
|
+
if (code !== 0)
|
|
164
|
+
throw new Error('the DirectX End-User Runtime installer failed (exit ' + code + ')');
|
|
165
|
+
}
|
|
166
|
+
export async function setupUnreal(env, config) {
|
|
167
|
+
if (!config.acceptLicense)
|
|
168
|
+
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/)');
|
|
169
|
+
const bad = config.components.find((id) => !ID.test(id));
|
|
170
|
+
if (bad)
|
|
171
|
+
throw new Error('toolchains.unreal.components: "' + bad + '" is not a Visual Studio component ID');
|
|
172
|
+
const ids = [...new Set([...UNREAL_COMPONENTS, ...config.components])];
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
log('(dry run) would install Visual Studio Community 2022 with ' + ids.join(', ') + ' and DirectX');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!installedVs())
|
|
178
|
+
await installVisualStudio(config, ids);
|
|
179
|
+
await installDirectX();
|
|
180
|
+
const vs = installedVs();
|
|
181
|
+
env.summary.push('unreal: Visual Studio Community ' +
|
|
182
|
+
vs.version +
|
|
183
|
+
' (C++ game workload, Windows 11 SDK, Unreal support) at ' +
|
|
184
|
+
vs.path +
|
|
185
|
+
'; DirectX End-User Runtime; get the engine from the Epic Games Launcher (Start menu)');
|
|
186
|
+
}
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -17,6 +17,9 @@ export const MAX_PLUGIN_BYTES = 1024 * 1024 * 1024;
|
|
|
17
17
|
const MAX_SETTINGS_BYTES = 4 * 1024 * 1024;
|
|
18
18
|
const MAX_DEPTH = 24;
|
|
19
19
|
const INSTALLED = 'installed_plugins.json';
|
|
20
|
+
/** What Claude rewrites when a plugin or marketplace is added, removed or updated: equal files, same plugins. */
|
|
21
|
+
const RECORDS = [INSTALLED, 'known_marketplaces.json'];
|
|
22
|
+
const MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
20
23
|
function isObject(value) {
|
|
21
24
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
25
|
}
|
|
@@ -69,7 +72,7 @@ export class PluginShare {
|
|
|
69
72
|
/** When a sandbox ends: its plugins refresh the shared set. Never throws; returns the problem, if any. */
|
|
70
73
|
saveQuietly(dataPath, claudeDir) {
|
|
71
74
|
try {
|
|
72
|
-
if (PluginShare.hasPlugins(claudeDir))
|
|
75
|
+
if (PluginShare.hasPlugins(claudeDir) && !this.matches(claudeDir))
|
|
73
76
|
this.save(dataPath, claudeDir);
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
@@ -131,9 +134,27 @@ export class PluginShare {
|
|
|
131
134
|
merged[key] = { ...shared[key], ...(isObject(merged[key]) ? merged[key] : {}) };
|
|
132
135
|
writeSandboxJson(file, merged);
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether the shared set already holds this Claude folder's plugins: the same plugin records and plugin settings.
|
|
139
|
+
* Copying a big plugins folder takes seconds, so a switch between accounts with the same plugins skips it.
|
|
140
|
+
*/
|
|
141
|
+
matches(claudeDir) {
|
|
142
|
+
if (!this.hasShared())
|
|
143
|
+
return false;
|
|
144
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
145
|
+
for (const name of RECORDS) {
|
|
146
|
+
const own = readUntrusted(path.join(claudeDir, 'plugins', name), MAX_RECORD_BYTES);
|
|
147
|
+
if (!same(own, readUntrusted(path.join(this.pluginsDir, name), MAX_RECORD_BYTES)))
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const shared = readUntrusted(this.settingsFile, MAX_SETTINGS_BYTES);
|
|
151
|
+
return same(pluginSettings(claudeDir), isObject(shared) ? shared : {});
|
|
152
|
+
}
|
|
134
153
|
/** One step when an account is switched to. */
|
|
135
154
|
sync(dataPath, claudeDir) {
|
|
136
155
|
if (PluginShare.hasPlugins(claudeDir)) {
|
|
156
|
+
if (this.matches(claudeDir))
|
|
157
|
+
return 'nothing';
|
|
137
158
|
this.save(dataPath, claudeDir);
|
|
138
159
|
return 'saved';
|
|
139
160
|
}
|
|
@@ -8,12 +8,12 @@ export interface SecretBox {
|
|
|
8
8
|
export declare class SecretStore {
|
|
9
9
|
private readonly box;
|
|
10
10
|
constructor(box: SecretBox);
|
|
11
|
-
/** Windows DPAPI, current user. */
|
|
12
|
-
static dpapi(): SecretStore;
|
|
11
|
+
/** Windows DPAPI, current user; with a storage folder, through its HostTools.exe (much faster than PowerShell). */
|
|
12
|
+
static dpapi(storageRoot?: string | null): SecretStore;
|
|
13
13
|
/** The test stand-in (not secure). */
|
|
14
14
|
static memory(): SecretStore;
|
|
15
15
|
/** What the program uses: DPAPI, except during the self-test (it never spawns Windows for this). */
|
|
16
|
-
static standard(): SecretStore;
|
|
16
|
+
static standard(storageRoot?: string | null): SecretStore;
|
|
17
17
|
/** True when `value` looks like something seal() made (any box). */
|
|
18
18
|
static isSealed(value: unknown): value is string;
|
|
19
19
|
seal(text: string): string;
|
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
// Encryption at rest for what must not sit in plain files (saved logins, the VPN login). The real box is Windows DPAPI
|
|
2
|
-
// for the current user (native\scripts\protect-data.ps1: data on standard input, never on
|
|
3
|
-
// box is the self-test's stand-in and is not secure. A sealed value is a short string:
|
|
2
|
+
// for the current user (HostTools.exe protect, else native\scripts\protect-data.ps1: data on standard input, never on
|
|
3
|
+
// a command line); the memory box is the self-test's stand-in and is not secure. A sealed value is a short string:
|
|
4
|
+
// "<kind>:<base64>".
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { fail } from '../../core/util/index.js';
|
|
6
7
|
import { SYSTEM } from '../../shared/system.js';
|
|
7
8
|
import { selfTestRunning } from '../../shared/test-guard.js';
|
|
8
|
-
import { nativeScript } from '../native.js';
|
|
9
|
+
import { hostToolsExe, nativeScript } from '../native.js';
|
|
9
10
|
const TIMEOUT_MS = 60000;
|
|
10
|
-
/**
|
|
11
|
+
/** HostTools.exe per storage folder (null: it cannot be built there), found once per process. */
|
|
12
|
+
const toolsFor = new Map();
|
|
13
|
+
function dpapiTool(storageRoot) {
|
|
14
|
+
if (!toolsFor.has(storageRoot)) {
|
|
15
|
+
let exe = null;
|
|
16
|
+
try {
|
|
17
|
+
exe = hostToolsExe(storageRoot);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Its bin folder cannot be written (another user's storage): PowerShell does the work.
|
|
21
|
+
}
|
|
22
|
+
toolsFor.set(storageRoot, exe);
|
|
23
|
+
}
|
|
24
|
+
return toolsFor.get(storageRoot) ?? null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* DPAPI for the current Windows user: HostTools.exe (built in <storageRoot>\bin; about 0.15 s a call) when a storage
|
|
28
|
+
* folder is known, else powershell.exe 5.1 (about 2 s a call, which a menu feels). Both read and write the same format.
|
|
29
|
+
*/
|
|
11
30
|
class DpapiBox {
|
|
31
|
+
storageRoot;
|
|
12
32
|
kind = 'dpapi1';
|
|
33
|
+
constructor(storageRoot) {
|
|
34
|
+
this.storageRoot = storageRoot;
|
|
35
|
+
}
|
|
13
36
|
seal(plain) {
|
|
14
37
|
return this.run('protect', plain.toString('base64'));
|
|
15
38
|
}
|
|
@@ -17,7 +40,11 @@ class DpapiBox {
|
|
|
17
40
|
return Buffer.from(this.run('unprotect', sealed), 'base64');
|
|
18
41
|
}
|
|
19
42
|
run(mode, input) {
|
|
20
|
-
const
|
|
43
|
+
const tools = this.storageRoot ? dpapiTool(this.storageRoot) : null;
|
|
44
|
+
const [exe, args] = tools
|
|
45
|
+
? [tools, [mode]]
|
|
46
|
+
: [SYSTEM.powershell(), nativeScript('protect-data.ps1', ['-Mode', mode])];
|
|
47
|
+
const result = spawnSync(exe, args, {
|
|
21
48
|
input,
|
|
22
49
|
encoding: 'utf8',
|
|
23
50
|
windowsHide: true,
|
|
@@ -49,17 +76,17 @@ export class SecretStore {
|
|
|
49
76
|
constructor(box) {
|
|
50
77
|
this.box = box;
|
|
51
78
|
}
|
|
52
|
-
/** Windows DPAPI, current user. */
|
|
53
|
-
static dpapi() {
|
|
54
|
-
return new SecretStore(new DpapiBox());
|
|
79
|
+
/** Windows DPAPI, current user; with a storage folder, through its HostTools.exe (much faster than PowerShell). */
|
|
80
|
+
static dpapi(storageRoot = null) {
|
|
81
|
+
return new SecretStore(new DpapiBox(storageRoot));
|
|
55
82
|
}
|
|
56
83
|
/** The test stand-in (not secure). */
|
|
57
84
|
static memory() {
|
|
58
85
|
return new SecretStore(new MemoryBox());
|
|
59
86
|
}
|
|
60
87
|
/** What the program uses: DPAPI, except during the self-test (it never spawns Windows for this). */
|
|
61
|
-
static standard() {
|
|
62
|
-
return selfTestRunning() ? SecretStore.memory() : SecretStore.dpapi();
|
|
88
|
+
static standard(storageRoot = null) {
|
|
89
|
+
return selfTestRunning() ? SecretStore.memory() : SecretStore.dpapi(storageRoot);
|
|
63
90
|
}
|
|
64
91
|
/** True when `value` looks like something seal() made (any box). */
|
|
65
92
|
static isSealed(value) {
|
package/dist/host/vpn/login.js
CHANGED
|
@@ -27,7 +27,7 @@ export function loginProblem(c) {
|
|
|
27
27
|
export class VpnLogin {
|
|
28
28
|
storageRoot;
|
|
29
29
|
secrets;
|
|
30
|
-
constructor(storageRoot, secrets = SecretStore.standard()) {
|
|
30
|
+
constructor(storageRoot, secrets = SecretStore.standard(storageRoot)) {
|
|
31
31
|
this.storageRoot = storageRoot;
|
|
32
32
|
this.secrets = secrets;
|
|
33
33
|
}
|
package/dist/runtime/agent.js
CHANGED
|
@@ -25486,7 +25486,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
25486
25486
|
"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."
|
|
25487
25487
|
),
|
|
25488
25488
|
vGpu: external_exports.boolean().describe(
|
|
25489
|
-
"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)."
|
|
25489
|
+
"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)."
|
|
25490
25490
|
),
|
|
25491
25491
|
networking: external_exports.boolean().describe("Internet access. false is the only hard network isolation Windows Sandbox offers."),
|
|
25492
25492
|
clipboard: external_exports.boolean().describe(
|
|
@@ -25648,6 +25648,21 @@ var ToolchainsSchema = external_exports.strictObject({
|
|
|
25648
25648
|
msvcVersion: external_exports.string().min(1).describe('"latest" or an MSVC tools version prefix, e.g. "14.44".'),
|
|
25649
25649
|
sdkVersion: external_exports.string().min(1).describe('"latest" or a Windows SDK build, e.g. "26100" or "10.0.22621".')
|
|
25650
25650
|
}),
|
|
25651
|
+
unreal: external_exports.strictObject({
|
|
25652
|
+
enabled: enabled(
|
|
25653
|
+
"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.",
|
|
25654
|
+
"a .uproject or .uplugin"
|
|
25655
|
+
),
|
|
25656
|
+
acceptLicense: external_exports.boolean().describe(
|
|
25657
|
+
"Required to install: you accept the Visual Studio Community licence terms (https://visualstudio.microsoft.com/license-terms/) and the DirectX End-User Runtime licence."
|
|
25658
|
+
),
|
|
25659
|
+
components: StringsSchema.describe(
|
|
25660
|
+
'More Visual Studio workload or component IDs to add, e.g. ["Microsoft.VisualStudio.Component.VC.Llvm.Clang"].'
|
|
25661
|
+
),
|
|
25662
|
+
keepInstallFiles: external_exports.boolean().describe(
|
|
25663
|
+
"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."
|
|
25664
|
+
)
|
|
25665
|
+
}),
|
|
25651
25666
|
cpp: external_exports.strictObject({
|
|
25652
25667
|
enabled: enabled(
|
|
25653
25668
|
"MinGW-w64 GCC with CMake, Ninja, premake5 and mingw32-make (also Rust's linker).",
|