windows-development-sandbox 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +22 -6
- package/config.example.json +17 -0
- package/dist/api.js +2 -1
- package/dist/cli/args.d.ts +20 -0
- package/dist/cli/args.js +35 -3
- package/dist/cli/colors.d.ts +24 -0
- package/dist/cli/colors.js +42 -0
- package/dist/cli/commands/agent-commands.d.ts +6 -0
- package/dist/cli/commands/agent-commands.js +61 -0
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +3 -0
- package/dist/cli/commands/data/clean.d.ts +15 -0
- package/dist/cli/commands/data/clean.js +101 -0
- package/dist/cli/commands/data/collect.d.ts +11 -0
- package/dist/cli/commands/data/collect.js +54 -0
- package/dist/cli/commands/data/index.d.ts +4 -0
- package/dist/cli/commands/data/index.js +45 -0
- package/dist/cli/commands/data/remove.d.ts +2 -0
- package/dist/cli/commands/data/remove.js +30 -0
- package/dist/cli/commands/index.d.ts +5 -3
- package/dist/cli/commands/index.js +5 -3
- package/dist/cli/commands/launch.d.ts +26 -2
- package/dist/cli/commands/launch.js +220 -39
- package/dist/cli/commands/maintenance.js +33 -4
- package/dist/cli/commands/project.d.ts +29 -0
- package/dist/cli/commands/project.js +63 -0
- package/dist/cli/commands/projects.d.ts +14 -0
- package/dist/cli/commands/projects.js +146 -0
- package/dist/cli/commands/repair/checks.d.ts +2 -0
- package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
- package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
- package/dist/cli/commands/repair/finders.js +108 -0
- package/dist/cli/commands/repair/index.d.ts +12 -0
- package/dist/cli/commands/repair/index.js +61 -0
- package/dist/cli/commands/repair/types.d.ts +26 -0
- package/dist/cli/commands/repair/types.js +3 -0
- package/dist/cli/commands/restart.d.ts +6 -0
- package/dist/cli/commands/restart.js +85 -0
- package/dist/cli/commands/sessions.d.ts +2 -1
- package/dist/cli/commands/sessions.js +189 -45
- package/dist/cli/commands/setup/checks.d.ts +13 -0
- package/dist/cli/commands/setup/checks.js +71 -0
- package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
- package/dist/cli/commands/setup/facts.js +60 -0
- package/dist/cli/commands/setup/index.d.ts +2 -0
- package/dist/cli/commands/setup/index.js +43 -0
- package/dist/cli/commands/user-config.d.ts +8 -0
- package/dist/cli/commands/user-config.js +41 -0
- package/dist/cli/history.d.ts +11 -0
- package/dist/cli/history.js +21 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.js +38 -13
- package/dist/cli/manifest.d.ts +8 -0
- package/dist/cli/manifest.js +35 -15
- package/dist/cli/menu-multi.d.ts +12 -0
- package/dist/cli/menu-multi.js +82 -0
- package/dist/cli/menu.d.ts +5 -13
- package/dist/cli/menu.js +149 -46
- package/dist/cli/progress.d.ts +9 -0
- package/dist/cli/progress.js +49 -0
- package/dist/cli/settings/edit.d.ts +13 -0
- package/dist/cli/settings/edit.js +19 -0
- package/dist/cli/settings/index.d.ts +8 -0
- package/dist/cli/settings/index.js +131 -0
- package/dist/cli/settings/prompts.d.ts +7 -0
- package/dist/cli/settings/prompts.js +48 -0
- package/dist/cli/settings/schema.d.ts +33 -0
- package/dist/cli/settings/schema.js +29 -0
- package/dist/cli/validate.d.ts +17 -0
- package/dist/cli/validate.js +89 -0
- package/dist/core/config.js +4 -1
- package/dist/core/disk.d.ts +16 -0
- package/dist/core/disk.js +55 -0
- package/dist/core/local-config.d.ts +8 -0
- package/dist/core/local-config.js +18 -5
- package/dist/core/project-policy.d.ts +15 -0
- package/dist/core/project-policy.js +35 -0
- package/dist/core/prompt.d.ts +4 -0
- package/dist/core/prompt.js +15 -0
- package/dist/core/safe-paths.d.ts +4 -0
- package/dist/core/safe-paths.js +37 -0
- package/dist/core/schema/common.d.ts +17 -0
- package/dist/core/schema/common.js +15 -0
- package/dist/core/schema/config.d.ts +594 -0
- package/dist/core/schema/config.js +63 -0
- package/dist/core/schema/databases.d.ts +33 -0
- package/dist/core/schema/databases.js +35 -0
- package/dist/core/schema/harness.d.ts +12 -0
- package/dist/core/schema/harness.js +17 -0
- package/dist/core/schema/json-schema.d.ts +5 -0
- package/dist/core/schema/json-schema.js +25 -0
- package/dist/core/schema/profile.d.ts +27 -0
- package/dist/core/schema/profile.js +29 -0
- package/dist/core/schema/projects.d.ts +9 -0
- package/dist/core/schema/projects.js +11 -0
- package/dist/core/schema/sandbox.d.ts +50 -0
- package/dist/core/schema/sandbox.js +36 -0
- package/dist/core/schema/toolchains.d.ts +93 -0
- package/dist/core/schema/toolchains.js +83 -0
- package/dist/core/schema.d.ts +6 -604
- package/dist/core/schema.js +8 -264
- package/dist/core/types.d.ts +29 -1
- package/dist/core/util.d.ts +7 -0
- package/dist/core/util.js +12 -3
- package/dist/core/workspace.d.ts +0 -5
- package/dist/core/workspace.js +12 -46
- package/dist/guest/boot-env.d.ts +4 -0
- package/dist/guest/boot-env.js +25 -0
- package/dist/guest/cmdline.d.ts +12 -0
- package/dist/guest/cmdline.js +23 -0
- package/dist/guest/daemon/exec.d.ts +5 -0
- package/dist/guest/daemon/exec.js +32 -0
- package/dist/guest/daemon/project.d.ts +14 -0
- package/dist/guest/daemon/project.js +132 -0
- package/dist/guest/daemon/session.d.ts +13 -0
- package/dist/guest/daemon/session.js +139 -0
- package/dist/guest/daemon/shutdown.d.ts +1 -0
- package/dist/guest/daemon/shutdown.js +21 -0
- package/dist/guest/daemon/state.d.ts +17 -0
- package/dist/guest/daemon/state.js +31 -0
- package/dist/guest/daemon.js +8 -141
- package/dist/guest/db.js +18 -3
- package/dist/guest/env.d.ts +2 -0
- package/dist/guest/env.js +4 -2
- package/dist/guest/guide.d.ts +6 -2
- package/dist/guest/guide.js +5 -4
- package/dist/guest/helpers.d.ts +3 -0
- package/dist/guest/helpers.js +26 -0
- package/dist/guest/main.js +17 -7
- package/dist/guest/ports.d.ts +6 -0
- package/dist/guest/ports.js +22 -0
- package/dist/guest/project-context.d.ts +7 -0
- package/dist/guest/project-context.js +10 -0
- package/dist/guest/project-env.d.ts +20 -0
- package/dist/guest/project-env.js +20 -0
- package/dist/guest/project-status.d.ts +5 -0
- package/dist/guest/project-status.js +17 -0
- package/dist/guest/ptyhost.d.ts +0 -18
- package/dist/guest/ptyhost.js +5 -63
- package/dist/guest/setup/agents.d.ts +5 -2
- package/dist/guest/setup/agents.js +22 -8
- package/dist/guest/setup/claude.js +11 -3
- package/dist/guest/setup/environment.d.ts +13 -3
- package/dist/guest/setup/environment.js +14 -10
- package/dist/guest/setup/hooks.d.ts +9 -1
- package/dist/guest/setup/hooks.js +28 -14
- package/dist/guest/toolchains/catalogue.d.ts +4 -0
- package/dist/guest/toolchains/catalogue.js +2 -2
- package/dist/guest/toolchains/cli.js +25 -17
- package/dist/guest/toolchains/common.d.ts +18 -1
- package/dist/guest/toolchains/common.js +34 -11
- package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
- package/dist/guest/toolchains/databases/dynamodb.js +52 -0
- package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
- package/dist/guest/toolchains/databases/index.js +62 -0
- package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
- package/dist/guest/toolchains/databases/mongodb.js +31 -0
- package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
- package/dist/guest/toolchains/databases/mysql.js +38 -0
- package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
- package/dist/guest/toolchains/databases/postgres.js +38 -0
- package/dist/guest/toolchains/databases/redis.d.ts +3 -0
- package/dist/guest/toolchains/databases/redis.js +16 -0
- package/dist/guest/toolchains/databases/server.d.ts +14 -0
- package/dist/guest/toolchains/databases/server.js +52 -0
- package/dist/guest/toolchains/git.js +25 -16
- package/dist/guest/toolchains/go.js +1 -1
- package/dist/guest/toolchains/index.d.ts +13 -9
- package/dist/guest/toolchains/index.js +40 -82
- package/dist/guest/toolchains/java.js +5 -5
- package/dist/guest/toolchains/leftovers.d.ts +2 -0
- package/dist/guest/toolchains/leftovers.js +26 -0
- package/dist/guest/toolchains/managers.js +12 -10
- package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
- package/dist/guest/toolchains/native/cpp.d.ts +4 -0
- package/dist/guest/toolchains/native/cpp.js +38 -0
- package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
- package/dist/guest/toolchains/native/dotnet.js +103 -0
- package/dist/guest/toolchains/native/mesa.d.ts +7 -0
- package/dist/guest/toolchains/native/mesa.js +46 -0
- package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
- package/dist/guest/toolchains/native/msvc/environment.js +57 -0
- package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
- package/dist/guest/toolchains/native/msvc/index.js +48 -0
- package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
- package/dist/guest/toolchains/native/msvc/packages.js +156 -0
- package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
- package/dist/guest/toolchains/native/msvc/tree.js +21 -0
- package/dist/guest/toolchains/native/rust.d.ts +3 -0
- package/dist/guest/toolchains/native/rust.js +45 -0
- package/dist/guest/toolchains/node.js +26 -6
- package/dist/guest/toolchains/plan.d.ts +22 -0
- package/dist/guest/toolchains/plan.js +96 -0
- package/dist/guest/toolchains/pwsh.js +1 -1
- package/dist/guest/toolchains/python.js +17 -10
- package/dist/guest/toolchains/runtimes.d.ts +2 -0
- package/dist/guest/toolchains/runtimes.js +6 -8
- package/dist/guest/toolchains/scripting/php.d.ts +3 -0
- package/dist/guest/toolchains/scripting/php.js +71 -0
- package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
- package/dist/guest/toolchains/scripting/ruby.js +47 -0
- package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
- package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
- package/dist/guest/view.js +21 -1
- package/dist/host/accounts.js +14 -5
- package/dist/host/agent-commands.d.ts +25 -0
- package/dist/host/agent-commands.js +145 -0
- package/dist/host/attach.d.ts +17 -0
- package/dist/host/attach.js +69 -0
- package/dist/host/channel.d.ts +23 -22
- package/dist/host/channel.js +66 -84
- package/dist/host/claude-profile.js +7 -3
- package/dist/host/cleanup.js +2 -11
- package/dist/host/database-plan.d.ts +12 -0
- package/dist/host/database-plan.js +52 -0
- package/dist/host/generate.d.ts +4 -12
- package/dist/host/generate.js +48 -135
- package/dist/host/join.d.ts +28 -0
- package/dist/host/join.js +76 -0
- package/dist/host/launch/mappings.d.ts +3 -0
- package/dist/host/launch/mappings.js +50 -0
- package/dist/host/launch/projects.d.ts +15 -0
- package/dist/host/launch/projects.js +56 -0
- package/dist/host/launch/stage.d.ts +11 -0
- package/dist/host/launch/stage.js +66 -0
- package/dist/host/launch/wsb.d.ts +10 -0
- package/dist/host/launch/wsb.js +34 -0
- package/dist/host/pc-scan.d.ts +6 -0
- package/dist/host/pc-scan.js +37 -0
- package/dist/host/platform.d.ts +2 -0
- package/dist/host/platform.js +13 -1
- package/dist/host/project-scan.d.ts +1 -17
- package/dist/host/project-scan.js +2 -86
- package/dist/host/projects.d.ts +100 -0
- package/dist/host/projects.js +147 -0
- package/dist/host/repo-guard.d.ts +15 -2
- package/dist/host/repo-guard.js +103 -12
- package/dist/host/terminal.js +3 -0
- package/dist/host/test-guard.d.ts +6 -0
- package/dist/host/test-guard.js +15 -0
- package/dist/host/window.js +7 -3
- package/dist/host/wsb.d.ts +37 -0
- package/dist/host/wsb.js +101 -0
- package/dist/runtime/daemon.js +10997 -2794
- package/dist/runtime/db.js +6428 -464
- package/dist/runtime/main.js +7359 -1135
- package/dist/runtime/view.js +5922 -47
- package/dist/selftest/accounts.test.d.ts +2 -0
- package/dist/selftest/accounts.test.js +44 -0
- package/dist/selftest/agent-commands.test.d.ts +2 -0
- package/dist/selftest/agent-commands.test.js +47 -0
- package/dist/selftest/channel.test.d.ts +2 -0
- package/dist/selftest/channel.test.js +37 -0
- package/dist/selftest/cli.test.d.ts +2 -2
- package/dist/selftest/cli.test.js +115 -27
- package/dist/selftest/cmdline.test.d.ts +2 -0
- package/dist/selftest/cmdline.test.js +45 -0
- package/dist/selftest/config.test.d.ts +2 -3
- package/dist/selftest/config.test.js +36 -72
- package/dist/selftest/data.test.d.ts +2 -2
- package/dist/selftest/data.test.js +77 -70
- package/dist/selftest/detect.test.d.ts +2 -2
- package/dist/selftest/detect.test.js +54 -38
- package/dist/selftest/fixtures.d.ts +44 -4
- package/dist/selftest/fixtures.js +40 -23
- package/dist/selftest/generate.test.d.ts +2 -0
- package/dist/selftest/generate.test.js +63 -0
- package/dist/selftest/git-access.test.d.ts +2 -0
- package/dist/selftest/git-access.test.js +26 -0
- package/dist/selftest/guest-boot.test.d.ts +2 -0
- package/dist/selftest/guest-boot.test.js +76 -0
- package/dist/selftest/harness.d.ts +44 -7
- package/dist/selftest/harness.js +62 -21
- package/dist/selftest/host-sim.test.d.ts +2 -0
- package/dist/selftest/host-sim.test.js +93 -0
- package/dist/selftest/hostile-input.test.d.ts +2 -0
- package/dist/selftest/hostile-input.test.js +54 -0
- package/dist/selftest/index.d.ts +8 -1
- package/dist/selftest/index.js +134 -35
- package/dist/selftest/links.test.d.ts +2 -0
- package/dist/selftest/links.test.js +50 -0
- package/dist/selftest/live-daemon.test.d.ts +2 -0
- package/dist/selftest/live-daemon.test.js +91 -0
- package/dist/selftest/live-git.test.d.ts +2 -0
- package/dist/selftest/live-git.test.js +60 -0
- package/dist/selftest/live-projects.test.d.ts +2 -0
- package/dist/selftest/live-projects.test.js +127 -0
- package/dist/selftest/live.d.ts +65 -0
- package/dist/selftest/live.js +109 -0
- package/dist/selftest/local-config.test.d.ts +2 -0
- package/dist/selftest/local-config.test.js +107 -0
- package/dist/selftest/mirror.test.d.ts +2 -3
- package/dist/selftest/mirror.test.js +92 -65
- package/dist/selftest/network.test.d.ts +2 -0
- package/dist/selftest/network.test.js +28 -0
- package/dist/selftest/personalization.test.d.ts +2 -0
- package/dist/selftest/personalization.test.js +53 -0
- package/dist/selftest/projects.test.d.ts +2 -0
- package/dist/selftest/projects.test.js +254 -0
- package/dist/selftest/repair.test.d.ts +2 -0
- package/dist/selftest/repair.test.js +42 -0
- package/dist/selftest/repo-guard.test.d.ts +2 -0
- package/dist/selftest/repo-guard.test.js +63 -0
- package/dist/selftest/secrets.test.d.ts +2 -0
- package/dist/selftest/secrets.test.js +71 -0
- package/dist/selftest/setup.test.d.ts +2 -2
- package/dist/selftest/setup.test.js +45 -22
- package/dist/selftest/suite.d.ts +13 -0
- package/dist/selftest/suite.js +1 -0
- package/dist/selftest/sync.test.d.ts +2 -3
- package/dist/selftest/sync.test.js +90 -78
- package/dist/selftest/test-guard.test.d.ts +2 -0
- package/dist/selftest/test-guard.test.js +49 -0
- package/dist/selftest/toolchains.test.d.ts +2 -0
- package/dist/selftest/toolchains.test.js +103 -0
- package/dist/selftest/workspace.test.d.ts +2 -0
- package/dist/selftest/workspace.test.js +53 -0
- package/dist/shared/download.d.ts +3 -1
- package/dist/shared/download.js +19 -4
- package/dist/shared/paths.d.ts +5 -0
- package/dist/shared/paths.js +6 -0
- package/dist/shared/projects.d.ts +53 -0
- package/dist/shared/projects.js +67 -0
- package/dist/shared/rundir.d.ts +10 -1
- package/dist/shared/rundir.js +12 -4
- package/dist/shared/safe-remove.js +11 -0
- package/dist/shared/schema.d.ts +443 -0
- package/dist/shared/schema.js +41 -2
- package/dist/shared/test-guard.d.ts +4 -0
- package/dist/shared/test-guard.js +37 -0
- package/dist/sync/claude-mem.js +3 -1
- package/dist/sync/history.d.ts +23 -12
- package/dist/sync/history.js +52 -20
- package/dist/sync/watcher.d.ts +8 -0
- package/dist/sync/watcher.js +23 -4
- package/package.json +6 -6
- package/sandbox/bootstrap.ps1 +258 -258
- package/sandbox/cpucap.ps1 +51 -0
- package/sandbox/logon.cmd +18 -16
- package/sandbox/openssl.cs +42 -0
- package/sandbox/ptyhost.cs +15 -4
- package/sandbox/trust.cmd +35 -35
- package/schemas/config.schema.json +100 -1
- package/schemas/sandbox-config.schema.json +17 -1
- package/dist/cli/commands/data.d.ts +0 -46
- package/dist/cli/commands/data.js +0 -249
- package/dist/cli/commands/setup.js +0 -152
- package/dist/cli/settings.d.ts +0 -11
- package/dist/cli/settings.js +0 -156
- package/dist/guest/toolchains/databases.js +0 -226
- package/dist/guest/toolchains/msvc.d.ts +0 -10
- package/dist/guest/toolchains/msvc.js +0 -216
- package/dist/guest/toolchains/native.d.ts +0 -12
- package/dist/guest/toolchains/native.js +0 -220
- package/dist/guest/toolchains/scripting.d.ts +0 -10
- package/dist/guest/toolchains/scripting.js +0 -137
- package/dist/selftest/guest.test.d.ts +0 -3
- package/dist/selftest/guest.test.js +0 -61
- package/dist/selftest/helpers.test.d.ts +0 -2
- package/dist/selftest/helpers.test.js +0 -125
- package/dist/selftest/host.test.d.ts +0 -2
- package/dist/selftest/host.test.js +0 -71
- package/dist/selftest/live.test.d.ts +0 -7
- package/dist/selftest/live.test.js +0 -127
- package/dist/selftest/security.test.d.ts +0 -3
- package/dist/selftest/security.test.js +0 -143
- /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// A project's .sandbox-config.json: it must be approved before it applies, it can never loosen protections, and it is
|
|
2
|
+
// validated. The project file is untrusted input (it arrives with a cloned repository).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { download, setChecksumRequired } from '../shared/download.js';
|
|
7
|
+
export const localConfigSuite = {
|
|
8
|
+
name: 'local-config',
|
|
9
|
+
title: '.sandbox-config.json: approval, tighten-only rules, validation',
|
|
10
|
+
run: async (t, f) => {
|
|
11
|
+
t.section('approval');
|
|
12
|
+
const localB = {
|
|
13
|
+
harness: 'codex',
|
|
14
|
+
sandbox: { memoryMb: 4096, networking: false, headless: true },
|
|
15
|
+
toolchains: { node: { version: '18' }, rust: { enabled: false } },
|
|
16
|
+
mappings: [{ host: './assets', guest: 'C:\\Assets', readOnly: true }, { host: '../shared', guest: 'C:\\Shared', readOnly: false }],
|
|
17
|
+
scripts: ['./setup.mjs'],
|
|
18
|
+
};
|
|
19
|
+
f.writeLocal(f.workB, localB);
|
|
20
|
+
const untrusted = await t.thrownMessage(() => f.run({ cwd: f.workB }));
|
|
21
|
+
t.includes(untrusted, '--trust-config', 'an untrusted .sandbox-config.json is refused, with the way to approve it');
|
|
22
|
+
t.includes(untrusted, 'READ/WRITE', 'the refusal lists what the file would change (read/write folders)');
|
|
23
|
+
await t.throws(() => f.run({ cwd: f.workB, ask: async () => false }), 'has not been approved', 'answering "no" refuses it');
|
|
24
|
+
const approved = await f.run({ cwd: f.workB, ask: async () => true });
|
|
25
|
+
t.equal({
|
|
26
|
+
harness: approved.harnessName,
|
|
27
|
+
memoryMb: approved.config.sandbox.memoryMb,
|
|
28
|
+
headless: approved.config.sandbox.headless,
|
|
29
|
+
rustEnabled: approved.config.toolchains.rust.enabled,
|
|
30
|
+
pythonStillOn: Boolean(approved.config.toolchains.python.enabled),
|
|
31
|
+
nodeSource: approved.versions.node.source,
|
|
32
|
+
}, { harness: 'codex', memoryMb: 4096, headless: true, rustEnabled: false, pythonStillOn: true, nodeSource: '.sandbox-config.json' }, 'approved local settings apply over the defaults (including toolchains and headless)');
|
|
33
|
+
t.check(approved.config.mappings.some((m) => m.host === f.shared && !m.readOnly), 'relative mapping paths resolve against the folder of .sandbox-config.json', 'mappings: ' + JSON.stringify(approved.config.mappings));
|
|
34
|
+
t.check(approved.scripts.includes(path.join(f.workB, 'setup.mjs')), 'relative script paths resolve against the folder of .sandbox-config.json', 'scripts: ' + JSON.stringify(approved.scripts));
|
|
35
|
+
t.equal((await f.run({ cwd: f.workB })).harnessName, 'codex', 'an approval is remembered');
|
|
36
|
+
t.equal((await f.run({ cwd: f.workB, noLocalConfig: true })).harnessName, 'claude', '--no-local-config ignores the file');
|
|
37
|
+
t.equal((await f.run({ cwd: f.workB, harness: 'gemini' })).harnessName, 'gemini', '--harness beats the file');
|
|
38
|
+
t.equal((await f.run({ cwd: f.workB, headless: false })).config.sandbox.headless, false, '--window beats a local headless setting');
|
|
39
|
+
f.writeLocal(f.workB, { ...localB, sandbox: { memoryMb: 6144 } });
|
|
40
|
+
await t.throws(() => f.run({ cwd: f.workB }), 'has not been approved', 'changing the file requires approval again');
|
|
41
|
+
t.section('validation');
|
|
42
|
+
const dirC = t.dir('work-c');
|
|
43
|
+
const rejectedBecause = (content, needle, name) => {
|
|
44
|
+
f.writeLocal(dirC, content);
|
|
45
|
+
return t.throws(() => f.run({ cwd: dirC, trustConfig: true }), needle, name);
|
|
46
|
+
};
|
|
47
|
+
await rejectedBecause({ storage: { root: 'C:\\x' } }, 'storage is not allowed', 'a project file cannot change the storage folder');
|
|
48
|
+
await rejectedBecause({ harnesses: {} }, 'harnesses is not allowed', 'a project file cannot change agent executables');
|
|
49
|
+
await rejectedBecause({ claudeProfile: { hostDir: 'C:\\x' } }, 'claudeProfile.hostDir is not allowed', 'a project file cannot point at the Claude profile folder');
|
|
50
|
+
await rejectedBecause({ toolchains: { node: { hostDir: 'x' } } }, 'toolchains.node.hostDir is not allowed', 'a project file cannot point at host tool folders');
|
|
51
|
+
await rejectedBecause({ sandbox: { memoryMb: 12 } }, 'sandbox.memoryMb:', 'values are type-checked (memory too small)');
|
|
52
|
+
await rejectedBecause({ mappings: [{ host: 'x', guest: 'C:\\Windows\\x', readOnly: true }] }, 'reserved or invalid guest path', 'reserved guest paths are rejected');
|
|
53
|
+
await rejectedBecause({ provisioning: { git: { access: 'admin' } } }, 'provisioning.git.access:', 'unknown git access modes are rejected');
|
|
54
|
+
await rejectedBecause('{\n // a comment\n "readOnly": tru\n}', 'line 3', 'broken JSON is reported with its line');
|
|
55
|
+
await rejectedBecause({ _comment: 'old style' }, '_comment is not allowed', 'unknown keys are rejected');
|
|
56
|
+
f.writeLocal(dirC, '// comments and trailing commas are fine\n{\n "readOnly": true, /* inline */\n}\n');
|
|
57
|
+
t.equal((await f.run({ cwd: dirC, trustConfig: true })).config.readOnly, true, '.sandbox-config.json may contain comments and trailing commas');
|
|
58
|
+
t.section('a project can only tighten protections');
|
|
59
|
+
// Settings that would loosen a protection are listed before approval, then ignored.
|
|
60
|
+
f.writeLocal(dirC, {
|
|
61
|
+
claudeProfile: { copyCredentials: true },
|
|
62
|
+
network: { blockPrivateRanges: false },
|
|
63
|
+
provisioning: { git: { access: 'full' } },
|
|
64
|
+
sandbox: { vGpu: true, clipboard: true, networking: true, protectedClient: false },
|
|
65
|
+
readOnly: false,
|
|
66
|
+
});
|
|
67
|
+
await t.throws(() => f.run({ cwd: dirC }), 'only used if it is stricter', 'settings a project may only tighten are called out before approval');
|
|
68
|
+
const strictUser = {
|
|
69
|
+
...f.baseConfig,
|
|
70
|
+
readOnly: true,
|
|
71
|
+
network: { ...f.baseConfig.network, blockPrivateRanges: true },
|
|
72
|
+
provisioning: { ...f.baseConfig.provisioning, git: { ...f.baseConfig.provisioning.git, access: 'readonly' } },
|
|
73
|
+
sandbox: { ...f.baseConfig.sandbox, vGpu: false, clipboard: false, networking: false, protectedClient: true },
|
|
74
|
+
};
|
|
75
|
+
const kept = (await f.run({ cwd: dirC, trustConfig: true }, strictUser)).config;
|
|
76
|
+
t.equal({
|
|
77
|
+
readOnly: kept.readOnly,
|
|
78
|
+
blockPrivateRanges: kept.network.blockPrivateRanges,
|
|
79
|
+
gitAccess: kept.provisioning.git.access,
|
|
80
|
+
vGpu: kept.sandbox.vGpu,
|
|
81
|
+
clipboard: kept.sandbox.clipboard,
|
|
82
|
+
networking: kept.sandbox.networking,
|
|
83
|
+
protectedClient: kept.sandbox.protectedClient,
|
|
84
|
+
copyCredentials: kept.claudeProfile.copyCredentials,
|
|
85
|
+
}, { readOnly: true, blockPrivateRanges: true, gitAccess: 'readonly', vGpu: false, clipboard: false, networking: false, protectedClient: true, copyCredentials: false }, 'a project file can never loosen network, git, read-only, GPU, clipboard or login settings');
|
|
86
|
+
f.writeLocal(dirC, { readOnly: true, network: { blockPrivateRanges: true }, provisioning: { git: { access: 'none' } }, sandbox: { networking: false, clipboard: false } });
|
|
87
|
+
const tightened = (await f.run({ cwd: dirC, trustConfig: true })).config;
|
|
88
|
+
t.equal({ readOnly: tightened.readOnly, gitAccess: tightened.provisioning.git.access, networking: tightened.sandbox.networking, clipboard: tightened.sandbox.clipboard }, { readOnly: true, gitAccess: 'none', networking: false, clipboard: false }, 'a project file can still make things stricter');
|
|
89
|
+
t.section('downloads.requireChecksum');
|
|
90
|
+
f.writeLocal(dirC, { downloads: { requireChecksum: false } });
|
|
91
|
+
const keptOn = (await f.run({ cwd: dirC, trustConfig: true }, { ...f.baseConfig, downloads: { requireChecksum: true } })).config;
|
|
92
|
+
f.writeLocal(dirC, { downloads: { requireChecksum: true } });
|
|
93
|
+
const turnedOn = (await f.run({ cwd: dirC, trustConfig: true })).config;
|
|
94
|
+
t.equal(keptOn.downloads.requireChecksum, true, 'a project file cannot switch the checksum requirement off');
|
|
95
|
+
t.equal(turnedOn.downloads.requireChecksum, true, 'a project file can switch the checksum requirement on');
|
|
96
|
+
t.equal(f.baseConfig.downloads.requireChecksum, false, '(the requirement is off in the defaults)');
|
|
97
|
+
// With the requirement on, a download without a hash is refused before anything is fetched.
|
|
98
|
+
setChecksumRequired(true);
|
|
99
|
+
const target = path.join(os.tmpdir(), 'agent-sandbox-no-such-download.bin');
|
|
100
|
+
const refused = await download('http://127.0.0.1:9/tool.zip', target).then(() => '', (e) => e.message);
|
|
101
|
+
setChecksumRequired(false);
|
|
102
|
+
t.includes(refused, 'requireChecksum', 'a download with no hash is refused when checksums are required');
|
|
103
|
+
t.includes(refused, 'tool.zip', 'the refusal names the download');
|
|
104
|
+
t.check(!fs.existsSync(target) && !fs.existsSync(target + '.part'), 'a refused download leaves nothing behind');
|
|
105
|
+
fs.rmSync(path.join(dirC, '.sandbox-config.json'));
|
|
106
|
+
},
|
|
107
|
+
};
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare const mirrorTests: (t: Harness, f: Fixtures) => Promise<void>;
|
|
1
|
+
import type { Suite } from './suite.js';
|
|
2
|
+
export declare const mirrorSuite: Suite;
|
|
@@ -3,72 +3,99 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Config, Mapping } from '../core/schema.js';
|
|
5
5
|
import { guestPathFor } from '../cli/commands/context.js';
|
|
6
|
-
import { buildWsb } from '../host/
|
|
6
|
+
import { buildWsb } from '../host/launch/wsb.js';
|
|
7
7
|
import { guestDrive, isValidGuestPath, planMirrorWorkspace, sandboxFolder, substDrives } from '../shared/guest-paths.js';
|
|
8
8
|
import { guestWorkspaceAliases, projectFolderName } from '../shared/paths.js';
|
|
9
9
|
import { syncHistoryIn, syncHistoryOut } from '../sync/history.js';
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
10
|
+
export const mirrorSuite = {
|
|
11
|
+
name: 'mirror',
|
|
12
|
+
title: 'guest paths on other drive letters, guestWorkspace "mirror"',
|
|
13
|
+
run: async (t, f) => {
|
|
14
|
+
t.section('which guest paths are valid');
|
|
15
|
+
const valid = ['C:\\Assets', 'C:\\Workspace\\x', 'F:\\SteamLibrary\\steamapps\\common\\Game', 'f:\\x', 'Z:\\a b\\c (1)'];
|
|
16
|
+
t.equal(valid.filter((p) => !isValidGuestPath(p)), [], 'guest paths under C:\\ or on D: to Z: are accepted');
|
|
17
|
+
const invalid = ['C:\\', 'F:\\', 'F:', 'C:\\Windows\\x', 'C:\\Users\\me', 'c:\\program files\\x', 'C:\\AgentData\\x', 'C:\\Mirror', 'C:\\Mirror\\F\\x', 'A:\\x', 'B:\\x', 'C:\\a\\..\\Windows', 'F:\\a\\..\\b', 'F:\\a:b', 'F:\\a/b', 'F:\\a\\\\b', 'F:\\a.\\b', 'F:\\a|b', 'F:\\a\nb', '\\\\server\\share\\x', 'relative\\x', 'F:x'];
|
|
18
|
+
t.equal(invalid.filter(isValidGuestPath), [], 'drive roots, reserved folders, C:\\Mirror, A:/B:, .., streams, odd characters and UNC paths are refused');
|
|
19
|
+
const mappingAccepts = (guest) => Mapping.safeParse({ host: 'E:\\Games', guest, readOnly: true }).success;
|
|
20
|
+
t.check(mappingAccepts('F:\\SteamLibrary\\Game') && mappingAccepts('C:\\F'), 'schema: mappings[].guest may be on another drive letter');
|
|
21
|
+
t.check(!mappingAccepts('C:\\Mirror\\F\\x') && !mappingAccepts('F:\\'), 'schema: mappings[].guest may never be C:\\Mirror or a drive root');
|
|
22
|
+
const workspaceAccepts = (guestWorkspace) => Config.safeParse({ ...f.makeConfig(), guestWorkspace }).success;
|
|
23
|
+
t.check(workspaceAccepts('mirror') && workspaceAccepts('F:\\AgentProjects\\x'), 'schema: guestWorkspace accepts "mirror" and D:-Z: paths');
|
|
24
|
+
t.check(!workspaceAccepts('C:\\Users\\x'), 'schema: guestWorkspace refuses reserved paths');
|
|
25
|
+
t.equal([guestDrive('f:\\x'), guestDrive('\\\\s\\x')], ['F', null], 'drive letter of a guest path');
|
|
26
|
+
t.section('where a drive-letter path really lives');
|
|
27
|
+
t.equal([sandboxFolder('F:\\SteamLibrary\\Game'), sandboxFolder('f:\\x'), sandboxFolder('C:\\Assets')], ['C:\\Mirror\\F\\SteamLibrary\\Game', 'C:\\Mirror\\F\\x', 'C:\\Assets'], 'F:\\x really lives at C:\\Mirror\\F\\x; C: paths stay as they are');
|
|
28
|
+
t.equal(substDrives(['C:\\a', 'G:\\z', 'F:\\x', 'f:\\y']), [{ letter: 'F', target: 'C:\\Mirror\\F' }, { letter: 'G', target: 'C:\\Mirror\\G' }], 'one subst drive per used letter (never C:), sorted');
|
|
29
|
+
t.equal(guestWorkspaceAliases('F:\\AgentProjects\\mw2'), ['C:\\Mirror\\F\\AgentProjects\\mw2', 'F:\\AgentProjects\\mw2'], 'a mirrored workspace is also known by its real C:\\Mirror path');
|
|
30
|
+
t.equal(guestWorkspaceAliases('C:\\Workspace\\x').length, 1, 'a C: workspace has no alias');
|
|
31
|
+
const wsb = buildWsb(f.baseConfig, 4096, [
|
|
32
|
+
{ host: 'E:\\SteamLibrary\\steamapps\\common\\Game', guest: 'F:\\SteamLibrary\\steamapps\\common\\Game', readOnly: true },
|
|
33
|
+
{ host: 'D:\\Assets', guest: 'C:\\Assets', readOnly: false },
|
|
34
|
+
]);
|
|
35
|
+
t.includes(wsb, '<SandboxFolder>C:\\Mirror\\F\\SteamLibrary\\steamapps\\common\\Game</SandboxFolder>', 'the .wsb maps F:\\ guests under C:\\Mirror\\F');
|
|
36
|
+
t.includes(wsb, '<SandboxFolder>C:\\Assets</SandboxFolder>', 'the .wsb maps C:\\ guests as they are');
|
|
37
|
+
t.excludes(wsb, '<SandboxFolder>F:', 'the .wsb never maps a drive-letter folder directly');
|
|
38
|
+
t.section('guestWorkspace "mirror"');
|
|
39
|
+
const fallback = 'C:\\Workspace\\mw2-agent';
|
|
40
|
+
const onF = planMirrorWorkspace('F:\\AgentProjects\\mw2-agent', fallback);
|
|
41
|
+
t.equal(onF.guest, 'F:\\AgentProjects\\mw2-agent', 'a workspace on F: appears at its PC path (mirror + subst)');
|
|
42
|
+
t.includes(onF.note ?? '', 'C:\\Mirror\\F\\AgentProjects\\mw2-agent', 'a workspace on F: comes with a note saying where it really is');
|
|
43
|
+
t.equal(planMirrorWorkspace('f:\\AgentProjects\\x\\', fallback).guest, 'F:\\AgentProjects\\x', 'a trailing backslash and a lower-case letter are normalised');
|
|
44
|
+
const onC = planMirrorWorkspace('C:\\dev\\mw2-agent', fallback);
|
|
45
|
+
t.equal({ guest: onC.guest, note: onC.note }, { guest: 'C:\\dev\\mw2-agent', note: null }, 'a workspace on C: outside reserved folders keeps its path');
|
|
46
|
+
const reserved = planMirrorWorkspace('C:\\Users\\josh\\mw2-agent', fallback);
|
|
47
|
+
t.equal(reserved.guest, fallback, 'C:\\Users\\... falls back to C:\\Workspace\\<name>...');
|
|
48
|
+
t.includes(reserved.note ?? '', 'reserved', '... with a note');
|
|
49
|
+
t.equal(planMirrorWorkspace('\\\\server\\share\\mw2-agent', fallback).guest, fallback, 'UNC paths fall back');
|
|
50
|
+
t.equal(planMirrorWorkspace('A:\\mw2-agent', fallback).guest, fallback, 'A: and B: fall back');
|
|
51
|
+
const resolved = await f.run({}, { ...f.makeConfig(), guestWorkspace: 'mirror' });
|
|
52
|
+
const expected = planMirrorWorkspace(f.workA, 'C:\\Workspace\\work-a');
|
|
53
|
+
t.equal(resolved.config.guestWorkspace, expected.guest, 'resolveRun turns "mirror" into a concrete guest path');
|
|
54
|
+
t.check(expected.note === null || resolved.notes.includes(expected.note), 'resolveRun explains a fallback in run.notes', JSON.stringify(resolved.notes));
|
|
55
|
+
t.section('generating a sandbox with drive-letter folders');
|
|
56
|
+
const game = t.dir('steam', 'Game');
|
|
57
|
+
const base = await f.run();
|
|
58
|
+
const run = { ...base, config: { ...base.config, guestWorkspace: 'G:\\Projects\\work-a', mappings: [{ host: game, guest: 'F:\\SteamLibrary\\steamapps\\common\\Game', readOnly: true }] } };
|
|
59
|
+
const generated = f.generate(run);
|
|
60
|
+
const session = f.readJson(generated.sessionPath);
|
|
61
|
+
const text = fs.readFileSync(generated.wsbPath, 'utf8');
|
|
62
|
+
t.equal(session.workspace, 'G:\\Projects\\work-a', 'session.json: the workspace is at its drive-letter path');
|
|
63
|
+
t.equal(session.substDrives, [{ letter: 'F', target: 'C:\\Mirror\\F' }, { letter: 'G', target: 'C:\\Mirror\\G' }], 'session.json: subst drives are listed for bootstrap.ps1');
|
|
64
|
+
t.includes(text, '<SandboxFolder>C:\\Mirror\\G\\Projects\\work-a</SandboxFolder>', '.wsb: the workspace is under C:\\Mirror');
|
|
65
|
+
t.includes(text, '<SandboxFolder>C:\\Mirror\\F\\SteamLibrary\\steamapps\\common\\Game</SandboxFolder>', '.wsb: the mapping is under C:\\Mirror');
|
|
66
|
+
t.equal(f.readJson(f.generate(base).sessionPath).substDrives, [], 'no other drive letters: no subst drives');
|
|
67
|
+
const withMapping = (guest) => ({ ...run, config: { ...run.config, mappings: [{ host: game, guest, readOnly: true }] } });
|
|
68
|
+
await t.throws(() => f.generate(withMapping('C:\\Mirror\\F\\x')), 'not a usable sandbox folder', 'a mapping straight into C:\\Mirror is refused');
|
|
69
|
+
await t.throws(() => f.generate(withMapping('C:\\Windows\\x')), 'not a usable sandbox folder', 'a reserved guest folder is refused by the host too (not only the schema)');
|
|
70
|
+
await t.throws(() => f.generate(withMapping('g:\\projects\\work-a\\game')), 'overlapping', 'mappings overlapping on the same mirrored drive are refused');
|
|
71
|
+
t.section('exec, attach and history for a mirrored workspace');
|
|
72
|
+
const workspace = 'F:\\AgentProjects\\mw2-agent';
|
|
73
|
+
t.equal(guestPathFor(workspace + '\\crates\\engine', workspace, workspace), workspace + '\\crates\\engine', 'a host folder maps to the same guest folder');
|
|
74
|
+
const project = projectFolderName(workspace);
|
|
75
|
+
t.equal(project, 'F--AgentProjects-mw2-agent', 'the Claude project folder is the same on both sides');
|
|
76
|
+
const sides = f.historySides('mirror', workspace, workspace);
|
|
77
|
+
const hostProject = path.join(sides.hostClaudeDir, 'projects', project);
|
|
78
|
+
const guestProject = path.join(sides.guestClaudeDir, 'projects', project);
|
|
79
|
+
t.write(path.join(hostProject, 's1.jsonl'), '{"cwd":"F:\\\\AgentProjects\\\\mw2-agent","t":1}\n');
|
|
80
|
+
t.write(path.join(sides.hostClaudeDir, 'history.jsonl'), '{"display":"hi","timestamp":1,"project":"F:\\\\AgentProjects\\\\mw2-agent"}\n');
|
|
81
|
+
syncHistoryIn(sides);
|
|
82
|
+
t.includes(fs.readFileSync(path.join(guestProject, 's1.jsonl'), 'utf8'), 'F:\\\\AgentProjects\\\\mw2-agent', 'history in: paths are unchanged');
|
|
83
|
+
t.equal(fs.readdirSync(path.join(sides.guestClaudeDir, 'projects')).length, 1, 'history in: only the one project folder is created');
|
|
84
|
+
t.write(path.join(guestProject, 's2.jsonl'), '{"cwd":"F:\\\\AgentProjects\\\\mw2-agent","t":2}\n');
|
|
85
|
+
const realPathProject = path.join(sides.guestClaudeDir, 'projects', 'C--Mirror-F-AgentProjects-mw2-agent');
|
|
86
|
+
t.write(path.join(realPathProject, 's3.jsonl'), '{"cwd":"C:\\\\Mirror\\\\F\\\\AgentProjects\\\\mw2-agent","f":"C:/Mirror/F/AgentProjects/mw2-agent/src/main.rs"}\n');
|
|
87
|
+
fs.appendFileSync(path.join(sides.guestClaudeDir, 'history.jsonl'), '{"display":"real path","timestamp":2,"project":"C:\\\\Mirror\\\\F\\\\AgentProjects\\\\mw2-agent"}\n');
|
|
88
|
+
syncHistoryOut(sides);
|
|
89
|
+
const s3 = path.join(hostProject, 's3.jsonl');
|
|
90
|
+
t.check(fs.existsSync(path.join(hostProject, 's2.jsonl')), 'history out: transcripts under the PC path come back');
|
|
91
|
+
t.check(fs.existsSync(s3), 'history out: transcripts under the real C:\\Mirror path come back into the PC project folder');
|
|
92
|
+
t.check(!fs.existsSync(path.join(sides.hostClaudeDir, 'projects', 'C--Mirror-F-AgentProjects-mw2-agent')), 'history out: no C:\\Mirror project folder appears on the PC');
|
|
93
|
+
const s3Text = fs.existsSync(s3) ? fs.readFileSync(s3, 'utf8') : '';
|
|
94
|
+
t.includes(s3Text, '"cwd":"F:\\\\AgentProjects\\\\mw2-agent"', 'history out: C:\\Mirror\\F\\... paths are translated back to F:\\...');
|
|
95
|
+
t.includes(s3Text, 'F:/AgentProjects/mw2-agent/src/main.rs', 'history out: forward-slash paths are translated too');
|
|
96
|
+
t.excludes(s3Text, 'Mirror', 'history out: nothing mentions C:\\Mirror any more');
|
|
97
|
+
const promptHistory = fs.readFileSync(path.join(sides.hostClaudeDir, 'history.jsonl'), 'utf8');
|
|
98
|
+
t.includes(promptHistory, '"project":"F:\\\\AgentProjects\\\\mw2-agent"}', 'prompt history out: real paths are translated');
|
|
99
|
+
t.excludes(promptHistory, 'Mirror', 'prompt history out: nothing mentions C:\\Mirror');
|
|
100
|
+
},
|
|
74
101
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isSandboxAddress } from '../host/attach.js';
|
|
2
|
+
import { buildFirewallCommands, cidrToRange, ipv4DnsServers, parseDefaultGateways, parseDnsServers, subtractIps } from '../shared/netrules.js';
|
|
3
|
+
export const networkSuite = {
|
|
4
|
+
name: 'network',
|
|
5
|
+
title: 'firewall maths, gateway/DNS parsing, attach address checks',
|
|
6
|
+
run: (t) => {
|
|
7
|
+
t.section('firewall');
|
|
8
|
+
const [start172, end172] = cidrToRange('172.16.0.0/12');
|
|
9
|
+
const gateway = 172 * 16777216 + 28 * 65536 + 1;
|
|
10
|
+
const remaining = subtractIps([[start172, end172]], ['172.28.0.1']);
|
|
11
|
+
t.check(remaining.every(([start, end]) => gateway < start || gateway > end), 'the private block minus the gateway no longer contains the gateway');
|
|
12
|
+
t.excludes(buildFirewallCommands(['8.8.8.8']).join(), '8.8.8.8', 'public addresses are not part of the private-range rules');
|
|
13
|
+
t.section('reading Windows output');
|
|
14
|
+
t.equal(parseDefaultGateways(' 0.0.0.0 0.0.0.0 172.28.80.1 172.28.85.200 5\n'), ['172.28.80.1'], 'the default gateway is read from the route table');
|
|
15
|
+
t.equal(parseDnsServers(' DNS Servers . . . : 172.28.80.1\n 8.8.8.8\n NetBIOS : x\n'), ['172.28.80.1', '8.8.8.8'], 'DNS servers are read from ipconfig output');
|
|
16
|
+
t.equal(ipv4DnsServers(['172.28.80.1', '8.8.8.8:1053', '[::1]:53', 'fe80::1']), ['172.28.80.1', '8.8.8.8'], 'DNS servers from the resolver (any Windows language): IPv4 only, ports dropped');
|
|
17
|
+
t.section('attach only reaches into the sandbox');
|
|
18
|
+
const interfaces = {
|
|
19
|
+
'vEthernet (Default Switch)': [{ address: '172.20.0.1', netmask: '255.255.240.0', family: 'IPv4', internal: false, mac: '', cidr: null }],
|
|
20
|
+
Ethernet: [{ address: '192.168.1.10', netmask: '255.255.255.0', family: 'IPv4', internal: false, mac: '', cidr: null }],
|
|
21
|
+
};
|
|
22
|
+
t.check(isSandboxAddress('172.20.5.9', interfaces), 'an address in the Hyper-V subnet is the sandbox');
|
|
23
|
+
t.check(!isSandboxAddress('192.168.1.20', interfaces), 'a LAN address is not');
|
|
24
|
+
t.check(!isSandboxAddress('127.0.0.1', interfaces), 'loopback is not');
|
|
25
|
+
t.check(!isSandboxAddress('8.8.8.8', interfaces), 'an internet address is not');
|
|
26
|
+
t.check(!isSandboxAddress('172.20.0.1', interfaces), 'this PC\'s own address on that subnet is not');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Things written for Windows itself on this PC: the registry personalisation (.reg file) and the Windows Terminal
|
|
2
|
+
// PowerShell 7 profile. The Windows Terminal part is only tried on temp copies.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { toRegFile } from '../host/personalize.js';
|
|
6
|
+
import { PWSH_PROFILE_GUID, syncPwshProfile, withPwshProfile } from '../host/terminal.js';
|
|
7
|
+
export const personalizationSuite = {
|
|
8
|
+
name: 'personalization',
|
|
9
|
+
title: 'registry personalisation (.reg) and the Windows Terminal PowerShell 7 profile',
|
|
10
|
+
run: (t) => {
|
|
11
|
+
t.section('registry values');
|
|
12
|
+
const reg = toRegFile([
|
|
13
|
+
{ key: 'Control Panel\\Desktop', name: 'WallpaperStyle', kind: 'String', value: '10' },
|
|
14
|
+
{ key: 'X', name: 'D', kind: 'DWord', value: 4294967295 },
|
|
15
|
+
{ key: 'X', name: 'B', kind: 'Binary', value: Buffer.from([1, 255]).toString('base64') },
|
|
16
|
+
]);
|
|
17
|
+
t.includes(reg, '[HKEY_CURRENT_USER\\Control Panel\\Desktop]', 'the .reg file only writes under HKCU');
|
|
18
|
+
t.includes(reg, '"WallpaperStyle"="10"', 'string values are written as strings');
|
|
19
|
+
t.includes(reg, '"D"=dword:ffffffff', 'DWORD values are written in hex');
|
|
20
|
+
t.includes(reg, '"B"=hex:01,ff', 'binary values are written as hex bytes');
|
|
21
|
+
t.section('Windows Terminal profile');
|
|
22
|
+
const shown = withPwshProfile({ theme: 'dark', profiles: { defaults: { font: {} } } }, true);
|
|
23
|
+
const shownProfiles = shown?.profiles;
|
|
24
|
+
t.equal(shownProfiles?.list.length, 1, 'a PowerShell 7 profile is added once pwsh is installed');
|
|
25
|
+
t.equal(shownProfiles?.list[0].guid, PWSH_PROFILE_GUID, 'the added profile has our GUID');
|
|
26
|
+
t.includes(String(shownProfiles?.list[0].commandline), 'pwsh.exe', 'the added profile starts pwsh.exe');
|
|
27
|
+
t.check(Boolean(shownProfiles?.defaults) && shown?.theme === 'dark', 'the rest of the settings are kept');
|
|
28
|
+
t.equal(withPwshProfile(shown, true), null, 'nothing is rewritten when the profile is already there');
|
|
29
|
+
t.equal(withPwshProfile({ profiles: { defaults: {} } }, false), null, 'nothing is rewritten when pwsh was never wanted');
|
|
30
|
+
const edited = { profiles: { list: [{ guid: 'other' }, { guid: PWSH_PROFILE_GUID.toUpperCase(), name: 'My pwsh', fontSize: 14 }] } };
|
|
31
|
+
const hidden = withPwshProfile(edited, false)?.profiles;
|
|
32
|
+
t.equal({ hidden: hidden?.list[1].hidden, name: hidden?.list[1].name, fontSize: hidden?.list[1].fontSize, count: hidden?.list.length }, { hidden: true, name: 'My pwsh', fontSize: 14, count: 2 }, 'turning pwsh off hides the profile and keeps the user\'s edits');
|
|
33
|
+
const oldForm = withPwshProfile({ profiles: [{ guid: 'a' }] }, true)?.profiles;
|
|
34
|
+
t.equal(oldForm.list.length, 2, 'the old array form of "profiles" works');
|
|
35
|
+
t.section('Windows Terminal settings.json on disk (JSONC, temp copy)');
|
|
36
|
+
const data = t.at('wt-data');
|
|
37
|
+
const settings = t.write(path.join(data, 'tools', 'terminal', 'settings', 'settings.json'), '// edited in the sandbox\n{ "theme": "light", "profiles": { "list": [{ "guid": "{x}", }] } }');
|
|
38
|
+
const read = () => JSON.parse(fs.readFileSync(settings, 'utf8'));
|
|
39
|
+
syncPwshProfile(data, true);
|
|
40
|
+
const untouched = fs.readFileSync(settings, 'utf8');
|
|
41
|
+
t.check(untouched.startsWith('// edited'), 'untouched until pwsh is installed (comments and trailing commas survive)');
|
|
42
|
+
t.write(path.join(data, 'tools', 'pwsh', 'pwsh.exe'), 'MZ');
|
|
43
|
+
syncPwshProfile(data, true);
|
|
44
|
+
const added = read();
|
|
45
|
+
t.equal(added.theme, 'light', 'once pwsh is installed the profile is added and the user\'s settings are kept');
|
|
46
|
+
t.equal(added.profiles.list.map((p) => p.guid), ['{x}', PWSH_PROFILE_GUID], 'the profile is added after the existing ones');
|
|
47
|
+
syncPwshProfile(data, false);
|
|
48
|
+
t.equal(read().profiles.list[1].hidden, true, 'the profile is hidden when pwsh is turned off');
|
|
49
|
+
t.write(settings, '{ not json');
|
|
50
|
+
syncPwshProfile(data, true);
|
|
51
|
+
t.equal(fs.readFileSync(settings, 'utf8'), '{ not json', 'an unreadable settings file is left alone');
|
|
52
|
+
},
|
|
53
|
+
};
|