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
|
@@ -1,73 +1,80 @@
|
|
|
1
|
-
// `sandbox data`
|
|
1
|
+
// `sandbox data` and `sandbox clean`: what a clean target may name, how sizes and removal treat links, and which old
|
|
2
|
+
// launch folders, logs and downloads are pruned.
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
5
|
+
import { resolveCleanTargets } from '../cli/commands/data/clean.js';
|
|
6
|
+
import { removeTree } from '../cli/commands/data/remove.js';
|
|
7
|
+
import { findLinks } from '../cli/commands/repair/finders.js';
|
|
8
|
+
import { humanBytes, sizeOf } from '../core/disk.js';
|
|
9
|
+
import { pruneStorage } from '../host/cleanup.js';
|
|
10
|
+
export const dataSuite = {
|
|
11
|
+
name: 'data',
|
|
12
|
+
title: 'data clean targets, sizes and removal with links, storage cleanup',
|
|
13
|
+
run: async (t) => {
|
|
14
|
+
t.section('clean targets');
|
|
15
|
+
const storage = t.at('data-store');
|
|
16
|
+
const dataPath = path.join(storage, 'data', 'default');
|
|
17
|
+
const resolve = (...inputs) => resolveCleanTargets(inputs, dataPath, storage).map((r) => path.relative(storage, r.path));
|
|
18
|
+
const inData = (...parts) => path.join('data', 'default', ...parts);
|
|
19
|
+
t.equal(resolve('tools/go'), [inData('tools', 'go')], 'tools/<name> resolves inside the data folder');
|
|
20
|
+
t.equal(resolve('tools\\go'), [inData('tools', 'go')], 'either slash works');
|
|
21
|
+
t.equal(resolve('Tools/agents/claude'), [inData('tools', 'agents', 'claude')], 'tools/agents/<name> resolves, whatever the case');
|
|
22
|
+
t.equal(resolve('databases/postgres-18'), [inData('databases', 'postgres-18')], 'databases/<name> resolves');
|
|
23
|
+
t.equal(resolve('logs'), [inData('logs')], 'logs resolves');
|
|
24
|
+
t.equal(resolve('launches'), ['launches'], 'launches resolves to the storage folder\'s launches');
|
|
25
|
+
t.equal(resolve('state/claude'), [inData('state', 'claude')], 'state/<agent> resolves');
|
|
26
|
+
t.equal(resolve('tools', 'tools/go', 'tools/go'), [inData('tools')], 'nested and repeated targets collapse');
|
|
27
|
+
t.equal(resolve('all', 'logs'), [inData(), 'launches'], '`all` is the data folder plus launches');
|
|
28
|
+
t.check(Boolean(resolveCleanTargets(['state/claude'], dataPath, storage)[0].warning?.includes('login')), 'cleaning state warns about the login');
|
|
29
|
+
t.check(Boolean(resolveCleanTargets(['all'], dataPath, storage)[0].warning), 'cleaning everything carries a warning');
|
|
30
|
+
const badTargets = ['', '..', 'tools/..', 'tools/../../x', '../data', 'C:\\Windows', 'C:tools', '/etc', '\\\\server\\share', 'tools/*', 'tools/go:stream', 'tools/a/b', 'tools/agents/a/b', 'logs/guest.log', 'accounts', 'sync', 'windows', 'tools/.', 'tools/go.'];
|
|
31
|
+
const accepted = badTargets.filter((target) => {
|
|
32
|
+
try {
|
|
33
|
+
resolveCleanTargets([target], dataPath, storage);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
t.equal(accepted, [], 'rejected: .., absolute paths, drive letters, UNC, wildcards, streams, too deep, unknown areas');
|
|
41
|
+
await t.throws(() => resolveCleanTargets([], dataPath, storage), '', 'rejected: no target at all (that is not "everything")');
|
|
42
|
+
t.section('sizes and removal never follow links');
|
|
43
|
+
const sized = t.at('sized');
|
|
44
|
+
t.write(path.join(sized, 'a.bin'), 'x'.repeat(1000));
|
|
45
|
+
t.write(path.join(sized, 'sub', 'b.bin'), 'y'.repeat(24));
|
|
46
|
+
const outside = t.at('outside');
|
|
47
|
+
t.write(path.join(outside, 'keep.txt'), 'z'.repeat(5000));
|
|
48
|
+
let linked = false;
|
|
49
|
+
try {
|
|
50
|
+
fs.symlinkSync(outside, path.join(sized, 'sub', 'escape'), 'junction');
|
|
51
|
+
linked = true;
|
|
52
|
+
}
|
|
53
|
+
catch { /* no junctions here */ }
|
|
54
|
+
t.equal(sizeOf(sized), 1024, 'sizes count real files only' + (linked ? ' (a junction counts 0)' : ''));
|
|
55
|
+
t.equal([humanBytes(1536), humanBytes(0), humanBytes(3 * 1024 ** 3)], ['1.5 KB', '0 B', '3.0 GB'], 'sizes are shown in readable units');
|
|
56
|
+
if (linked)
|
|
57
|
+
t.equal(findLinks(sized).length, 1, 'repair finds a junction planted in the data folder');
|
|
58
|
+
removeTree(sized, t.temp);
|
|
59
|
+
t.check(!fs.existsSync(sized), 'a folder is removed');
|
|
60
|
+
t.check(fs.existsSync(path.join(outside, 'keep.txt')), 'removing a folder never deletes what a link inside points to');
|
|
61
|
+
t.section('storage cleanup');
|
|
62
|
+
const store = t.at('prune');
|
|
63
|
+
const aged = (p, days) => { const d = new Date(Date.now() - days * 86400000); fs.utimesSync(p, d, d); };
|
|
64
|
+
for (const [name, days] of [['old', 9], ['kept', 30], ['new', 1]]) {
|
|
65
|
+
aged(t.write(path.join(store, 'launches', name, 'agent.wsb'), 'x'), days);
|
|
66
|
+
aged(path.join(store, 'launches', name), days);
|
|
67
|
+
}
|
|
68
|
+
aged(t.write(path.join(store, 'data', 'd', 'logs', 'guest-old.log'), 'x'), 20);
|
|
69
|
+
t.write(path.join(store, 'data', 'd', 'logs', 'guest-new.log'), 'x');
|
|
70
|
+
const staging = t.dir('prune', 'data', 'd', 'tools', 'git.staging-ab12');
|
|
71
|
+
aged(staging, 2);
|
|
72
|
+
t.dir('prune', 'data', 'd', 'tools', 'node');
|
|
73
|
+
const pruned = pruneStorage(store, { keep: ['kept'] });
|
|
74
|
+
const left = (p) => fs.existsSync(path.join(store, p));
|
|
75
|
+
t.equal(pruned.launches, 1, 'cleanup removes one launch folder');
|
|
76
|
+
t.equal({ old: left('launches/old'), kept: left('launches/kept'), new: left('launches/new') }, { old: false, kept: true, new: true }, 'cleanup: launch folders older than 7 days go; the running launch and recent ones stay');
|
|
77
|
+
t.equal({ oldLog: left('data/d/logs/guest-old.log'), newLog: left('data/d/logs/guest-new.log') }, { oldLog: false, newLog: true }, 'cleanup: logs older than 14 days go');
|
|
78
|
+
t.equal({ interrupted: left('data/d/tools/git.staging-ab12'), realTool: left('data/d/tools/node') }, { interrupted: false, realTool: true }, 'cleanup: interrupted downloads go; real tools stay');
|
|
79
|
+
},
|
|
73
80
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
1
|
+
import type { Suite } from './suite.js';
|
|
2
|
+
export declare const detectSuite: Suite;
|
|
@@ -1,42 +1,58 @@
|
|
|
1
|
-
// "
|
|
1
|
+
// "What does this project need?": databases and toolchains detected from project files, and the plans built from them.
|
|
2
2
|
import { databasesFromServices, detectDatabases } from '../core/detect-databases.js';
|
|
3
3
|
import { DEFAULTS } from '../core/config.js';
|
|
4
|
-
import { databaseCatalog, planDatabases } from '../host/
|
|
5
|
-
import { voltaNode } from '../core/workspace.js';
|
|
4
|
+
import { databaseCatalog, planDatabases } from '../host/database-plan.js';
|
|
6
5
|
import { decideToolchains, resolveToolchains } from '../core/detect-toolchains.js';
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
export const
|
|
10
|
-
|
|
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
|
-
|
|
6
|
+
const found = (files, host = []) => detectDatabases(files, host).map((d) => d.database);
|
|
7
|
+
const file = (path, text) => ({ path, text });
|
|
8
|
+
export const detectSuite = {
|
|
9
|
+
name: 'detect',
|
|
10
|
+
title: 'database and toolchain detection from project files',
|
|
11
|
+
run: (t) => {
|
|
12
|
+
t.section('databases from project files');
|
|
13
|
+
const compose = file('docker-compose.yml', 'services:\n db:\n image: postgres:16-alpine\n cache:\n image: "redis:7"\n dynamo:\n image: amazon/dynamodb-local:latest\n');
|
|
14
|
+
t.equal(found([compose]), ['postgres', 'redis', 'dynamodb'], 'compose services: postgres, redis and DynamoDB Local images');
|
|
15
|
+
t.equal(found([file('package.json', '{"dependencies":{"pg":"^8.11.0","ioredis":"5","@aws-sdk/client-dynamodb":"3"}}')]), ['postgres', 'redis', 'dynamodb'], 'package.json: pg, ioredis, @aws-sdk/client-dynamodb');
|
|
16
|
+
t.equal(found([file('package.json', '{"dependencies":{"pgadmin-ui":"1","mysql-logger":"2","express":"4"}}')]), [], 'package.json: names that only contain a driver name do not count');
|
|
17
|
+
t.equal(found([file('prisma/schema.prisma', 'datasource db {\n provider = "mysql"\n url = env("DATABASE_URL")\n}')]), ['mysql'], 'Prisma: the datasource provider decides');
|
|
18
|
+
t.equal(found([file('requirements.txt', 'django==5.0\npsycopg2-binary==2.9\npymongo>=4\n')]), ['postgres', 'mongodb'], 'Python requirements: psycopg2 and pymongo');
|
|
19
|
+
t.equal(found([file('Api/Api.csproj', '<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />')]), ['sqlserver'], '.NET: EF Core SQL Server provider');
|
|
20
|
+
t.equal(found([file('.env.example', 'DATABASE_URL=postgresql://user:pass@localhost:5432/app\nMONGO_URL=mongodb+srv://cluster/x\n')]), ['postgres', 'mongodb'], '.env templates: connection-string schemes');
|
|
21
|
+
t.equal(found([file('template.yaml', 'Resources:\n Table:\n Type: AWS::DynamoDB::Table\n')]), ['dynamodb'], 'SAM/CloudFormation: a DynamoDB table resource');
|
|
22
|
+
t.equal(found([file('go.mod', 'require (\n\tgithub.com/jackc/pgx/v5 v5.5.0\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.0.0\n)')]), ['postgres', 'dynamodb'], 'go.mod: pgx and the DynamoDB SDK');
|
|
23
|
+
const knex = [file('package.json', '{"dependencies":{"knex":"3"}}')];
|
|
24
|
+
t.equal(found(knex), [], 'an ORM that names no database uses nothing when this PC has none');
|
|
25
|
+
t.equal(found(knex, ['mysql']), ['mysql'], 'an ORM that names no database uses the one installed on this PC');
|
|
26
|
+
t.equal(databasesFromServices(['postgresql-x64-16', 'MySQL80', 'MSSQL$SQLEXPRESS', 'Spooler', 'Redis']), ['postgres', 'mysql', 'sqlserver', 'redis'], 'database services on this PC are recognised by name');
|
|
27
|
+
// Hostile workspace files (a cloned repository is untrusted): detection must stay linear.
|
|
28
|
+
const hostile = [
|
|
29
|
+
file('.env', 'Server=a;Database=a;'.repeat(12000)),
|
|
30
|
+
file('compose.yaml', 'services:\n x:\n image: ' + 'postgres'.repeat(20000) + '!\n'),
|
|
31
|
+
file('appsettings.json', 'Host=a;'.repeat(30000)),
|
|
32
|
+
];
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
detectDatabases(hostile);
|
|
35
|
+
const took = Date.now() - started;
|
|
36
|
+
t.check(took < 2000, 'database detection stays fast on hostile workspace files (no catastrophic backtracking)', 'took ' + took + ' ms');
|
|
37
|
+
t.section('the database plan');
|
|
38
|
+
const plan = planDatabases({ ...DEFAULTS.databases, mongodb: { ...DEFAULTS.databases.mongodb, enabled: true }, redis: { ...DEFAULTS.databases.redis, enabled: false } }, 'unused', { files: [compose], host: [] });
|
|
39
|
+
t.equal(plan.map((p) => p.database + '@' + p.version + ':' + p.port), ['postgres@16:5432', 'mongodb@latest:27017', 'dynamodb@latest:8000'], 'plan: "auto" from the project (compose pins Postgres 16), true always, false never');
|
|
40
|
+
t.equal(plan.find((p) => p.database === 'dynamodb')?.adminPort, 8001, 'plan: DynamoDB gets its admin UI port');
|
|
41
|
+
const catalog = databaseCatalog({ ...DEFAULTS.databases, postgres: { ...DEFAULTS.databases.postgres, enabled: false, port: 6543 } });
|
|
42
|
+
t.equal(catalog.map((p) => p.database), ['postgres', 'mysql', 'mongodb', 'redis', 'dynamodb'], 'sbx-db catalog: every database that can run (not SQL Server), even ones off at boot');
|
|
43
|
+
t.equal(catalog[0].port, 6543, 'sbx-db catalog: configured ports are used');
|
|
44
|
+
t.section('toolchains from project files');
|
|
45
|
+
const tc = DEFAULTS.toolchains;
|
|
46
|
+
const goProject = decideToolchains(tc, ['README.md', 'go.mod', 'api/main.go'], ['python', 'rust']);
|
|
47
|
+
t.equal({ go: goProject.go.on, reason: goProject.go.reason }, { go: true, reason: 'go.mod' }, 'toolchains: a Go project gets Go (and says why)');
|
|
48
|
+
t.equal({ python: goProject.python.on, rust: goProject.rust.on, dotnet: goProject.dotnet.on, java: goProject.java.on }, { python: false, rust: false, dotnet: false, java: false }, 'toolchains: a Go project gets nothing it does not use, whatever this PC has');
|
|
49
|
+
const fresh = decideToolchains(tc, ['notes.txt'], ['python', 'dotnet']);
|
|
50
|
+
t.equal({ python: fresh.python.on, dotnet: fresh.dotnet.on, go: fresh.go.on }, { python: true, dotnet: true, go: false }, 'toolchains: a folder that is no project yet gets the languages this PC has');
|
|
51
|
+
t.includes(fresh.python.reason, 'this PC', 'toolchains: the reason names this PC');
|
|
52
|
+
const rusty = decideToolchains({ ...tc, python: { ...tc.python, enabled: true } }, ['Cargo.toml'], []);
|
|
53
|
+
t.equal({ rust: rusty.rust.on, cpp: rusty.cpp.on }, { rust: true, cpp: true }, 'toolchains: Rust brings its C/C++ linker');
|
|
54
|
+
t.equal({ python: rusty.python.on, reason: rusty.python.reason }, { python: true, reason: 'enabled in config' }, 'toolchains: true in config always wins');
|
|
55
|
+
const resolved = resolveToolchains(tc, goProject);
|
|
56
|
+
t.equal({ go: resolved.go.enabled, python: resolved.python.enabled }, { go: true, python: false }, 'the sandbox gets plain decisions instead of "auto"');
|
|
57
|
+
},
|
|
42
58
|
};
|
|
@@ -2,6 +2,20 @@ import type { Config, HostProbe, ResolvedRun, RunRequest } from '../core/types.j
|
|
|
2
2
|
import { generateSandbox } from '../host/generate.js';
|
|
3
3
|
import type { Harness } from './harness.js';
|
|
4
4
|
export declare const HOST_NODE_VERSION = "v22.3.0";
|
|
5
|
+
/** Colour codes are removed from captured output, so tests can match plain text. */
|
|
6
|
+
export declare const stripAnsi: (text: string) => string;
|
|
7
|
+
export interface NodeRun {
|
|
8
|
+
status: number | null;
|
|
9
|
+
out: string;
|
|
10
|
+
timedOut: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface NodeRunOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Runs `node <args>` and collects stdout + stderr (colours removed). Never throws; a hung process is killed at the timeout. */
|
|
18
|
+
export declare const runNode: (args: string[], options?: NodeRunOptions) => Promise<NodeRun>;
|
|
5
19
|
export declare const createFixtures: (t: Harness) => {
|
|
6
20
|
at: (...parts: string[]) => string;
|
|
7
21
|
hostNode: string;
|
|
@@ -26,6 +40,8 @@ export declare const createFixtures: (t: Harness) => {
|
|
|
26
40
|
sandbox: {
|
|
27
41
|
memoryMb: number | "auto";
|
|
28
42
|
maxMemoryMb: number;
|
|
43
|
+
maxMemoryPercent: number;
|
|
44
|
+
cpuLimit: number | "none";
|
|
29
45
|
reserveHostMb: number;
|
|
30
46
|
vGpu: boolean;
|
|
31
47
|
networking: boolean;
|
|
@@ -56,6 +72,9 @@ export declare const createFixtures: (t: Harness) => {
|
|
|
56
72
|
enabled: boolean;
|
|
57
73
|
};
|
|
58
74
|
};
|
|
75
|
+
downloads: {
|
|
76
|
+
requireChecksum: boolean;
|
|
77
|
+
};
|
|
59
78
|
toolchains: {
|
|
60
79
|
node: {
|
|
61
80
|
version: string;
|
|
@@ -184,6 +203,10 @@ export declare const createFixtures: (t: Harness) => {
|
|
|
184
203
|
auto: boolean;
|
|
185
204
|
autoHide: boolean;
|
|
186
205
|
};
|
|
206
|
+
agentCommands: {
|
|
207
|
+
enabled: boolean;
|
|
208
|
+
agents: string[] | "auto";
|
|
209
|
+
};
|
|
187
210
|
secrets: {
|
|
188
211
|
strip: boolean;
|
|
189
212
|
allow: string[];
|
|
@@ -217,16 +240,33 @@ export declare const createFixtures: (t: Harness) => {
|
|
|
217
240
|
hooks: {
|
|
218
241
|
failFast: boolean;
|
|
219
242
|
};
|
|
243
|
+
projects: {
|
|
244
|
+
all: boolean;
|
|
245
|
+
exclude: string[];
|
|
246
|
+
roots: string[];
|
|
247
|
+
askToAddRoot: boolean;
|
|
248
|
+
declinedRoots: string[];
|
|
249
|
+
wsbShare: boolean;
|
|
250
|
+
};
|
|
220
251
|
$schema?: string | undefined;
|
|
221
252
|
};
|
|
222
253
|
probe: HostProbe;
|
|
223
254
|
run: (over?: Partial<RunRequest>, config?: Config) => Promise<ResolvedRun>;
|
|
224
255
|
generate: (r: ResolvedRun, args?: string[], p?: HostProbe) => ReturnType<typeof generateSandbox>;
|
|
256
|
+
launch: (over?: Partial<RunRequest>, options?: {
|
|
257
|
+
config?: Config;
|
|
258
|
+
args?: string[];
|
|
259
|
+
probe?: HostProbe;
|
|
260
|
+
}) => Promise<ReturnType<typeof generateSandbox>>;
|
|
225
261
|
writeLocal: (dir: string, content: unknown) => void;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
262
|
+
historySides: (name: string, hostWorkspace: string, guestWorkspace: string) => {
|
|
263
|
+
hostClaudeDir: string;
|
|
264
|
+
guestClaudeDir: string;
|
|
265
|
+
hostWorkspace: string;
|
|
266
|
+
guestWorkspace: string;
|
|
229
267
|
};
|
|
230
|
-
|
|
268
|
+
runGuest: (launchPath: string, data: string, workspace: string, extra?: Record<string, string>) => Promise<NodeRun>;
|
|
269
|
+
runBin: (name: string, args: string[], options?: NodeRunOptions) => Promise<NodeRun>;
|
|
270
|
+
readJson: <T = any>(file: string) => T;
|
|
231
271
|
};
|
|
232
272
|
export type Fixtures = ReturnType<typeof createFixtures>;
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
// A fake "host PC" inside the temp folder: Node, claude.exe, a ~/.claude profile, workspaces, and helpers to resolve
|
|
2
|
-
// generate
|
|
3
|
-
import fs from 'node:fs';
|
|
1
|
+
// A fake "host PC" inside the temp folder: Node, claude.exe, a ~/.claude profile, workspaces, and helpers to resolve a
|
|
2
|
+
// run, generate the sandbox for it, run the guest against it and start the CLI. Shared by all suites.
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
6
5
|
import { DEFAULTS } from '../core/config.js';
|
|
7
6
|
import { generateSandbox } from '../host/generate.js';
|
|
8
|
-
import { ensureDir } from '../core/util.js';
|
|
9
7
|
import { resolveRun } from '../core/workspace.js';
|
|
10
8
|
export const HOST_NODE_VERSION = 'v22.3.0';
|
|
9
|
+
/** Colour codes are removed from captured output, so tests can match plain text. */
|
|
10
|
+
export const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
11
|
+
/** Runs `node <args>` and collects stdout + stderr (colours removed). Never throws; a hung process is killed at the timeout. */
|
|
12
|
+
export const runNode = (args, options = {}) => new Promise((resolve) => {
|
|
13
|
+
const child = spawn(process.execPath, args, { cwd: options.cwd, env: options.env ?? process.env, windowsHide: true });
|
|
14
|
+
let out = '';
|
|
15
|
+
let timedOut = false;
|
|
16
|
+
const timer = setTimeout(() => { timedOut = true; child.kill(); }, options.timeoutMs ?? 120000);
|
|
17
|
+
child.stdout.on('data', (c) => { out += c.toString(); });
|
|
18
|
+
child.stderr.on('data', (c) => { out += c.toString(); });
|
|
19
|
+
child.on('error', (e) => { out += String(e); });
|
|
20
|
+
child.on('close', (status) => { clearTimeout(timer); resolve({ status: timedOut ? null : status, out: stripAnsi(out), timedOut }); });
|
|
21
|
+
});
|
|
11
22
|
export const createFixtures = (t) => {
|
|
12
|
-
const {
|
|
13
|
-
|
|
23
|
+
const { at, write } = t;
|
|
24
|
+
// The PC: Node with npm, Claude Code, a ~/.claude profile (with a login that must never be copied) and ~/.claude.json.
|
|
14
25
|
const hostNode = at('host-node');
|
|
15
26
|
write(path.join(hostNode, 'node.exe'), 'x');
|
|
16
27
|
write(path.join(hostNode, 'node_modules', 'npm', 'bin', 'npm-cli.js'), '');
|
|
@@ -22,16 +33,18 @@ export const createFixtures = (t) => {
|
|
|
22
33
|
write(path.join(hostProfile, 'commands', 'review.md'), 'review cmd');
|
|
23
34
|
write(path.join(hostProfile, 'plugins', 'installed_plugins.json'), JSON.stringify({ path: hostProfile + '\\plugins\\cache\\claude-mem' }));
|
|
24
35
|
write(path.join(hostProfile, '.credentials.json'), '{ "token": "secret" }');
|
|
25
|
-
const hostGlobalState =
|
|
36
|
+
const hostGlobalState = t.writeJson(at('host-claude.json'), { hasCompletedOnboarding: true, mcpServers: { github: { command: 'npx' } } });
|
|
37
|
+
// The install folder, two workspaces (B has a setup script), a folder to share, the storage root, and tools "from this PC".
|
|
26
38
|
const install = at('install');
|
|
27
|
-
|
|
28
|
-
const workA =
|
|
39
|
+
t.dir('install', 'sub');
|
|
40
|
+
const workA = t.dir('work-a');
|
|
29
41
|
const workB = at('work-b');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
t.dir('work-b', 'assets');
|
|
43
|
+
const shared = t.dir('shared');
|
|
44
|
+
t.dir('storage');
|
|
33
45
|
write(path.join(workB, 'setup.mjs'), "import fs from 'node:fs'; import path from 'node:path'; fs.writeFileSync(path.join(process.env.AGENT_DATA, 'hook-ran.txt'), process.env.AGENT_WORKSPACE);\n");
|
|
34
46
|
write(at('host-tools', 'agents', 'claude', 'claude.exe'), 'fake-claude');
|
|
47
|
+
/** The default config, pointed at the fake PC. `copyFromHost = false` makes the sandbox download everything instead. */
|
|
35
48
|
const makeConfig = (copyFromHost = true) => {
|
|
36
49
|
const c = structuredClone(DEFAULTS);
|
|
37
50
|
c.storage.root = at('storage');
|
|
@@ -47,17 +60,21 @@ export const createFixtures = (t) => {
|
|
|
47
60
|
};
|
|
48
61
|
const baseConfig = makeConfig();
|
|
49
62
|
const probe = { nodeDir: hostNode, harnessExe: { dir: hostClaude, file: 'claude.exe' }, gitDir: null, nvmDir: null, hostNodeVersion: HOST_NODE_VERSION };
|
|
50
|
-
|
|
63
|
+
/** Resolves a run for `workA` (or whatever `over` says). */
|
|
64
|
+
const run = (over = {}, config = baseConfig) => resolveRun({ packageRoot: install, loaded: { config, configPath: null, base: t.temp }, cwd: workA, noAgent: false, noLocalConfig: false, trustConfig: false, scripts: [], ...over }, HOST_NODE_VERSION);
|
|
51
65
|
const generate = (r, args = [], p = probe) => generateSandbox({ packageRoot: t.packageRoot, run: r, harnessArgs: args, probe: p });
|
|
66
|
+
/** Resolves a run and generates its sandbox (.wsb + launch folder + session.json) in one step. */
|
|
67
|
+
const launch = async (over = {}, options = {}) => generate(await run(over, options.config ?? baseConfig), options.args, options.probe);
|
|
68
|
+
/** Writes a project's `.sandbox-config.json` (objects are serialised; strings are written as they are). */
|
|
52
69
|
const writeLocal = (dir, content) => { write(path.join(dir, '.sandbox-config.json'), typeof content === 'string' ? content : JSON.stringify(content)); };
|
|
70
|
+
/** The four folders history sync works with, for a workspace on the PC and its place in the sandbox. */
|
|
71
|
+
const historySides = (name, hostWorkspace, guestWorkspace) => ({ hostClaudeDir: at(name + '-host'), guestClaudeDir: at(name + '-guest'), hostWorkspace, guestWorkspace });
|
|
53
72
|
/** Runs the guest's bundled main.js in dry-run mode against fake sandbox folders. */
|
|
54
|
-
const runGuest = (launchPath, data, workspace, extra = {}) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
62
|
-
return { at, hostNode, hostClaude, hostProfile, hostGlobalState, install, workA, workB, shared, makeConfig, baseConfig, probe, run, generate, writeLocal, runGuest, readJson };
|
|
73
|
+
const runGuest = async (launchPath, data, workspace, extra = {}) => runNode([path.join(launchPath, 'runtime', 'main.js')], {
|
|
74
|
+
env: { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_HOST_TOOLS_DIR: at('host-tools'), AGENT_WORKSPACE: workspace, AGENT_LOCAL_DIR: at('local'), AGENT_MANAGED_DIR: at('managed'), AGENT_DRY_RUN: '1', ...extra },
|
|
75
|
+
});
|
|
76
|
+
/** Runs a built command (dist/bin/<name>.js) as its own process. */
|
|
77
|
+
const runBin = (name, args, options = {}) => runNode([path.join(t.packageRoot, 'dist', 'bin', name + '.js'), ...args], options);
|
|
78
|
+
const readJson = t.readJson;
|
|
79
|
+
return { at, hostNode, hostClaude, hostProfile, hostGlobalState, install, workA, workB, shared, makeConfig, baseConfig, probe, run, generate, launch, writeLocal, historySides, runGuest, runBin, readJson };
|
|
63
80
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What the host generates for a sandbox: the .wsb file, the launch folder (runtime, C# sources, boot scripts),
|
|
2
|
+
// session.json, the staged Claude profile, and the values read from this PC (git identity, free RAM).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { LocalOverrides } from '../core/schema.js';
|
|
6
|
+
import { gitIdentity } from '../host/launch/stage.js';
|
|
7
|
+
import { buildWsb, resolveMemoryMb } from '../host/launch/wsb.js';
|
|
8
|
+
export const generateSuite = {
|
|
9
|
+
name: 'generate',
|
|
10
|
+
title: 'generated .wsb, launch folder and session.json',
|
|
11
|
+
run: async (t, f) => {
|
|
12
|
+
t.section('the .wsb file');
|
|
13
|
+
const gen = await f.launch({}, { args: ['--version'], probe: { ...f.probe, gitDir: f.shared, nvmDir: f.workB } });
|
|
14
|
+
const wsb = fs.readFileSync(gen.wsbPath, 'utf8');
|
|
15
|
+
t.includes(wsb, '<MemoryInMB>8192</MemoryInMB>', 'wsb: the configured memory');
|
|
16
|
+
t.includes(wsb, '<SandboxFolder>C:\\Workspace\\work-a</SandboxFolder>', 'wsb: the workspace is mapped to its guest folder');
|
|
17
|
+
t.includes(wsb, 'C:\\HostTools\\git', 'wsb: this PC\'s git is mapped');
|
|
18
|
+
t.includes(wsb, 'C:\\HostTools\\nvm', 'wsb: this PC\'s nvm is mapped');
|
|
19
|
+
t.excludes(wsb, 'Missing', 'wsb: an optional mapping whose folder is missing is skipped');
|
|
20
|
+
t.includes(wsb, 'logon.cmd</Command>', 'wsb: the logon command');
|
|
21
|
+
t.includes(wsb, '<vGPU>Disable</vGPU>', 'wsb: vGPU is off');
|
|
22
|
+
t.includes(buildWsb(f.baseConfig, 4096, [{ host: 'C:\\a&b', guest: 'C:\\X', readOnly: true }]), 'C:\\a&b', 'wsb values are XML-escaped');
|
|
23
|
+
t.section('the launch folder');
|
|
24
|
+
const expectedFiles = ['package.json', 'runtime/main.js', 'runtime/daemon.js', 'runtime/view.js', 'runtime/gitguard.js', 'sandbox/bootstrap.ps1', 'sandbox/logon.cmd', 'sandbox/trust.cmd', 'sandbox/ptyhost.cs', 'sandbox/gitshim.cs'];
|
|
25
|
+
const missing = expectedFiles.filter((file) => !fs.existsSync(path.join(gen.launchPath, file)));
|
|
26
|
+
t.equal(missing, [], 'the launch folder has the runtime, the C# sources and the boot scripts');
|
|
27
|
+
t.equal(fs.readdirSync(path.join(gen.launchPath, 'runtime')).length, 5, 'the runtime is 5 self-contained files');
|
|
28
|
+
const logon = fs.readFileSync(path.join(gen.launchPath, 'sandbox', 'logon.cmd'), 'utf8');
|
|
29
|
+
t.check(logon.indexOf('trust.cmd') < logon.indexOf('wt.exe'), 'logon.cmd applies the sandbox trust settings before opening Windows Terminal');
|
|
30
|
+
t.includes(logon, 'tools\\terminal\\wt.exe', 'logon.cmd opens Windows Terminal from the tools folder');
|
|
31
|
+
t.includes(logon, '-NoExit', 'logon.cmd falls back to a PowerShell window');
|
|
32
|
+
t.section('session.json');
|
|
33
|
+
const session = f.readJson(gen.sessionPath);
|
|
34
|
+
t.equal({
|
|
35
|
+
launchId: session.launchId === gen.launchId,
|
|
36
|
+
workspace: session.workspace,
|
|
37
|
+
workspaceHost: session.workspaceHost,
|
|
38
|
+
firstArgument: session.harnessArguments[0],
|
|
39
|
+
gitFromHost: Boolean(session.hostTools.git),
|
|
40
|
+
nvmFromHost: Boolean(session.hostTools.nvm),
|
|
41
|
+
nodeSpec: session.versions.node.spec,
|
|
42
|
+
gitAccess: session.provisioning.git.access,
|
|
43
|
+
}, { launchId: true, workspace: 'C:\\Workspace\\work-a', workspaceHost: f.workA, firstArgument: '--version', gitFromHost: true, nvmFromHost: true, nodeSpec: '22.3.0', gitAccess: 'full' }, 'session.json carries the id, paths, agent arguments, host tools, versions and git access');
|
|
44
|
+
t.matches(session.hostTools.nodeVersion ?? '', /^\d+\.\d+\.\d+$/, 'session.json: the host Node version is a plain version');
|
|
45
|
+
t.equal(session.cpuLimitPercent, 80, 'session.json carries the CPU cap for cpucap.ps1 (default 80 %)');
|
|
46
|
+
t.check(!LocalOverrides.safeParse({ sandbox: { cpuLimit: 'none' } }).success, "a project's .sandbox-config.json cannot lift your CPU cap (sandbox.cpuLimit is yours only)");
|
|
47
|
+
t.section('what is copied from this PC');
|
|
48
|
+
const staged = path.join(gen.launchPath, 'claude-profile');
|
|
49
|
+
const stagedMissing = ['settings.json', 'CLAUDE.md', '.claude.json', 'plugins'].filter((name) => !fs.existsSync(path.join(staged, name)));
|
|
50
|
+
t.equal(stagedMissing, [], 'Claude settings, instructions, global state and plugins are staged');
|
|
51
|
+
t.check(!fs.existsSync(path.join(staged, '.credentials.json')), 'the Claude login is not staged');
|
|
52
|
+
const gitconfig = '[user]\n\tname = Josh\n\temail = "j@x.io"\n[credential]\n\thelper = store\n[url "https://TOKEN@github.com/"]\n\tinsteadOf = https://github.com/\n';
|
|
53
|
+
t.equal(gitIdentity(gitconfig), '[user]\n\tname = Josh\n\temail = j@x.io\n', 'only name/email from ~/.gitconfig reach the sandbox (no credential helpers or token URLs)');
|
|
54
|
+
t.equal(gitIdentity('[core]\n\tname = x\n'), '', 'a name outside [user] is not an identity');
|
|
55
|
+
const auto = { ...f.baseConfig.sandbox, memoryMb: 'auto' };
|
|
56
|
+
const MB = 1048576;
|
|
57
|
+
const pcRam = 64 * 1024 * MB;
|
|
58
|
+
t.equal(resolveMemoryMb(auto, 40000 * MB, pcRam), 31744, "memory 'auto' follows free RAM minus the reserve");
|
|
59
|
+
await t.throws(() => resolveMemoryMb(auto, 9000 * MB, pcRam), 'Not enough free RAM', "memory 'auto' refuses when too little RAM is free");
|
|
60
|
+
t.equal(resolveMemoryMb({ ...auto, maxMemoryPercent: 80 }, 30000 * MB, 32 * 1024 * MB), 21504, "memory 'auto' never takes more than maxMemoryPercent of the PC's RAM");
|
|
61
|
+
t.equal(resolveMemoryMb({ ...auto, maxMemoryPercent: 50 }, 60000 * MB, pcRam), 32768, "memory 'auto' is capped at half the PC's RAM with maxMemoryPercent 50 (free RAM and maxMemoryMb still apply)");
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Git access modes ("full", "readonly", "none"): which git commands the guard allows and the matching Claude deny rules.
|
|
2
|
+
import { gitDecision } from '../guest/gitguard.js';
|
|
3
|
+
import { gitDenyRules } from '../guest/gitrules.js';
|
|
4
|
+
export const gitAccessSuite = {
|
|
5
|
+
name: 'git-access',
|
|
6
|
+
title: 'git access modes and Claude deny rules',
|
|
7
|
+
run: (t) => {
|
|
8
|
+
t.section('readonly');
|
|
9
|
+
const allowed = (args, mode = 'readonly') => gitDecision(args, mode).allow;
|
|
10
|
+
const reading = ['status', 'log --oneline', 'diff HEAD~1', 'show HEAD', 'blame a.ts', 'branch', 'branch -a', 'branch --contains HEAD', 'tag', 'tag -l v*', 'remote -v', 'config --get user.name', 'stash list', '-C sub status', '--no-pager log'];
|
|
11
|
+
t.equal(reading.filter((command) => !allowed(command.split(' '))), [], 'readonly: reading commands are allowed');
|
|
12
|
+
const writing = ['commit -m x', 'push', 'add .', 'checkout main', 'switch -c x', 'reset --hard', 'stash', 'stash pop', 'branch new', 'branch -D x', 'tag v1', 'merge x', 'rebase main', 'fetch', 'pull', 'config user.name x', 'remote add o u', '-c core.hooksPath=x status', '--git-dir=x status', 'clean -fd', 'apply p.diff'];
|
|
13
|
+
t.equal(writing.filter((command) => allowed(command.split(' '))), [], 'readonly: anything that writes (or overrides config) is refused');
|
|
14
|
+
t.section('none and full');
|
|
15
|
+
t.check(!allowed(['status'], 'none') && !allowed(['--version'], 'none'), 'none: every git call is refused');
|
|
16
|
+
t.check(allowed(['commit'], 'full'), 'full: everything is allowed');
|
|
17
|
+
t.section('Claude managed deny rules');
|
|
18
|
+
const readonlyRules = gitDenyRules('readonly');
|
|
19
|
+
const noneRules = gitDenyRules('none');
|
|
20
|
+
t.equal(gitDenyRules('full').length, 0, 'full: no deny rules');
|
|
21
|
+
t.includes(readonlyRules.join('\n'), 'Bash(git push:*)', 'readonly: pushing is denied');
|
|
22
|
+
t.check(!readonlyRules.some((r) => r.startsWith('Read(')), 'readonly: reading .git is not denied');
|
|
23
|
+
t.check(noneRules.includes('Bash(git:*)'), 'none: every git command is denied');
|
|
24
|
+
t.check(noneRules.includes('Read(**/.git/**)'), 'none: reading .git is denied too');
|
|
25
|
+
},
|
|
26
|
+
};
|