windows-development-sandbox 0.0.0-stage → 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/.sandbox-config.example.json +14 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/config.example.json +349 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +31 -0
- package/dist/bin/agent-sandbox.d.ts +2 -0
- package/dist/bin/agent-sandbox.js +3 -0
- package/dist/bin/claude-host.d.ts +2 -0
- package/dist/bin/claude-host.js +5 -0
- package/dist/bin/claude-sbx.d.ts +2 -0
- package/dist/bin/claude-sbx.js +3 -0
- package/dist/bin/codex-sbx.d.ts +2 -0
- package/dist/bin/codex-sbx.js +3 -0
- package/dist/bin/gemini-sbx.d.ts +2 -0
- package/dist/bin/gemini-sbx.js +3 -0
- package/dist/bin/sandbox.d.ts +2 -0
- package/dist/bin/sandbox.js +4 -0
- package/dist/bin/sbx-hide.d.ts +2 -0
- package/dist/bin/sbx-hide.js +3 -0
- package/dist/bin/sbx-show.d.ts +2 -0
- package/dist/bin/sbx-show.js +3 -0
- package/dist/cli/args.d.ts +52 -0
- package/dist/cli/args.js +143 -0
- package/dist/cli/colors.d.ts +24 -0
- package/dist/cli/colors.js +42 -0
- package/dist/cli/commands/accounts.d.ts +2 -0
- package/dist/cli/commands/accounts.js +52 -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 +23 -0
- package/dist/cli/commands/context.js +57 -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 +10 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/launch.d.ts +29 -0
- package/dist/cli/commands/launch.js +333 -0
- package/dist/cli/commands/maintenance.d.ts +8 -0
- package/dist/cli/commands/maintenance.js +154 -0
- 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/checks.js +170 -0
- package/dist/cli/commands/repair/finders.d.ts +30 -0
- 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 +12 -0
- package/dist/cli/commands/sessions.js +257 -0
- package/dist/cli/commands/setup/checks.d.ts +13 -0
- package/dist/cli/commands/setup/checks.js +71 -0
- package/dist/cli/commands/setup/facts.d.ts +31 -0
- 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 +20 -0
- package/dist/cli/index.js +121 -0
- package/dist/cli/manifest.d.ts +103 -0
- package/dist/cli/manifest.js +107 -0
- package/dist/cli/menu-multi.d.ts +12 -0
- package/dist/cli/menu-multi.js +82 -0
- package/dist/cli/menu.d.ts +14 -0
- package/dist/cli/menu.js +290 -0
- 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.d.ts +17 -0
- package/dist/core/config.js +106 -0
- package/dist/core/detect-databases.d.ts +15 -0
- package/dist/core/detect-databases.js +147 -0
- package/dist/core/detect-pwsh.d.ts +18 -0
- package/dist/core/detect-pwsh.js +111 -0
- package/dist/core/detect-toolchains.d.ts +30 -0
- package/dist/core/detect-toolchains.js +58 -0
- package/dist/core/disk.d.ts +16 -0
- package/dist/core/disk.js +55 -0
- package/dist/core/local-config.d.ts +20 -0
- package/dist/core/local-config.js +77 -0
- 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 -0
- package/dist/core/schema.js +8 -0
- package/dist/core/types.d.ts +190 -0
- package/dist/core/types.js +1 -0
- package/dist/core/util.d.ts +60 -0
- package/dist/core/util.js +154 -0
- package/dist/core/workspace.d.ts +18 -0
- package/dist/core/workspace.js +128 -0
- 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.d.ts +1 -0
- package/dist/guest/daemon.js +53 -0
- package/dist/guest/db.d.ts +1 -0
- package/dist/guest/db.js +75 -0
- package/dist/guest/env.d.ts +51 -0
- package/dist/guest/env.js +101 -0
- package/dist/guest/gitguard.d.ts +7 -0
- package/dist/guest/gitguard.js +60 -0
- package/dist/guest/gitrules.d.ts +2 -0
- package/dist/guest/gitrules.js +11 -0
- package/dist/guest/guide.d.ts +16 -0
- package/dist/guest/guide.js +69 -0
- package/dist/guest/helpers.d.ts +3 -0
- package/dist/guest/helpers.js +26 -0
- package/dist/guest/main.d.ts +1 -0
- package/dist/guest/main.js +130 -0
- 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 +22 -0
- package/dist/guest/ptyhost.js +36 -0
- package/dist/guest/setup/agents.d.ts +11 -0
- package/dist/guest/setup/agents.js +87 -0
- package/dist/guest/setup/claude.d.ts +21 -0
- package/dist/guest/setup/claude.js +146 -0
- package/dist/guest/setup/environment.d.ts +16 -0
- package/dist/guest/setup/environment.js +69 -0
- package/dist/guest/setup/hooks.d.ts +9 -0
- package/dist/guest/setup/hooks.js +50 -0
- package/dist/guest/setup/network.d.ts +10 -0
- package/dist/guest/setup/network.js +99 -0
- package/dist/guest/setup/policy.d.ts +3 -0
- package/dist/guest/setup/policy.js +18 -0
- package/dist/guest/setup/user.d.ts +7 -0
- package/dist/guest/setup/user.js +47 -0
- package/dist/guest/toolchains/catalogue.d.ts +55 -0
- package/dist/guest/toolchains/catalogue.js +31 -0
- package/dist/guest/toolchains/cli.d.ts +10 -0
- package/dist/guest/toolchains/cli.js +72 -0
- package/dist/guest/toolchains/common.d.ts +54 -0
- package/dist/guest/toolchains/common.js +147 -0
- package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
- package/dist/guest/toolchains/databases/dynamodb.js +52 -0
- package/dist/guest/toolchains/databases/index.d.ts +6 -0
- 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.d.ts +3 -0
- package/dist/guest/toolchains/git.js +121 -0
- package/dist/guest/toolchains/go.d.ts +3 -0
- package/dist/guest/toolchains/go.js +33 -0
- package/dist/guest/toolchains/index.d.ts +17 -0
- package/dist/guest/toolchains/index.js +80 -0
- package/dist/guest/toolchains/java.d.ts +8 -0
- package/dist/guest/toolchains/java.js +96 -0
- package/dist/guest/toolchains/leftovers.d.ts +2 -0
- package/dist/guest/toolchains/leftovers.js +26 -0
- package/dist/guest/toolchains/managers.d.ts +16 -0
- package/dist/guest/toolchains/managers.js +84 -0
- package/dist/guest/toolchains/native/binstall.d.ts +7 -0
- package/dist/guest/toolchains/native/binstall.js +52 -0
- 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.d.ts +3 -0
- package/dist/guest/toolchains/node.js +152 -0
- package/dist/guest/toolchains/plan.d.ts +22 -0
- package/dist/guest/toolchains/plan.js +96 -0
- package/dist/guest/toolchains/pwsh.d.ts +4 -0
- package/dist/guest/toolchains/pwsh.js +14 -0
- package/dist/guest/toolchains/python.d.ts +3 -0
- package/dist/guest/toolchains/python.js +54 -0
- package/dist/guest/toolchains/runtimes.d.ts +19 -0
- package/dist/guest/toolchains/runtimes.js +38 -0
- 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/toolchains/versions.d.ts +47 -0
- package/dist/guest/toolchains/versions.js +77 -0
- package/dist/guest/toolchains/vscode.d.ts +2 -0
- package/dist/guest/toolchains/vscode.js +53 -0
- package/dist/guest/view.d.ts +1 -0
- package/dist/guest/view.js +42 -0
- package/dist/host/accounts.d.ts +32 -0
- package/dist/host/accounts.js +128 -0
- 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 +38 -0
- package/dist/host/channel.js +162 -0
- package/dist/host/claude-profile.d.ts +15 -0
- package/dist/host/claude-profile.js +107 -0
- package/dist/host/cleanup.d.ts +23 -0
- package/dist/host/cleanup.js +57 -0
- package/dist/host/database-plan.d.ts +12 -0
- package/dist/host/database-plan.js +52 -0
- package/dist/host/generate.d.ts +7 -0
- package/dist/host/generate.js +98 -0
- 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/personalize.d.ts +10 -0
- package/dist/host/personalize.js +97 -0
- package/dist/host/platform.d.ts +19 -0
- package/dist/host/platform.js +126 -0
- package/dist/host/project-scan.d.ts +7 -0
- package/dist/host/project-scan.js +89 -0
- package/dist/host/projects.d.ts +100 -0
- package/dist/host/projects.js +147 -0
- package/dist/host/repo-guard.d.ts +36 -0
- package/dist/host/repo-guard.js +219 -0
- package/dist/host/secrets.d.ts +14 -0
- package/dist/host/secrets.js +133 -0
- package/dist/host/terminal.d.ts +32 -0
- package/dist/host/terminal.js +130 -0
- package/dist/host/test-guard.d.ts +6 -0
- package/dist/host/test-guard.js +15 -0
- package/dist/host/window.d.ts +4 -0
- package/dist/host/window.js +72 -0
- package/dist/host/wsb.d.ts +37 -0
- package/dist/host/wsb.js +101 -0
- package/dist/runtime/daemon.js +28494 -0
- package/dist/runtime/db.js +26387 -0
- package/dist/runtime/gitguard.js +84 -0
- package/dist/runtime/main.js +28622 -0
- package/dist/runtime/view.js +25841 -0
- 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 -0
- package/dist/selftest/cli.test.js +118 -0
- 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 -0
- package/dist/selftest/config.test.js +38 -0
- package/dist/selftest/data.test.d.ts +2 -0
- package/dist/selftest/data.test.js +80 -0
- package/dist/selftest/detect.test.d.ts +2 -0
- package/dist/selftest/detect.test.js +58 -0
- package/dist/selftest/fixtures.d.ts +272 -0
- package/dist/selftest/fixtures.js +80 -0
- 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 +51 -0
- package/dist/selftest/harness.js +77 -0
- 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 -0
- package/dist/selftest/index.js +155 -0
- 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 -0
- package/dist/selftest/mirror.test.js +101 -0
- 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 -0
- package/dist/selftest/setup.test.js +46 -0
- package/dist/selftest/suite.d.ts +13 -0
- package/dist/selftest/suite.js +1 -0
- package/dist/selftest/sync.test.d.ts +2 -0
- package/dist/selftest/sync.test.js +94 -0
- 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 +60 -0
- package/dist/shared/download.js +215 -0
- package/dist/shared/guest-paths.d.ts +30 -0
- package/dist/shared/guest-paths.js +61 -0
- package/dist/shared/netrules.d.ts +18 -0
- package/dist/shared/netrules.js +98 -0
- package/dist/shared/paths.d.ts +14 -0
- package/dist/shared/paths.js +28 -0
- package/dist/shared/projects.d.ts +53 -0
- package/dist/shared/projects.js +67 -0
- package/dist/shared/pty-client.d.ts +86 -0
- package/dist/shared/pty-client.js +227 -0
- package/dist/shared/rundir.d.ts +39 -0
- package/dist/shared/rundir.js +94 -0
- package/dist/shared/safe-remove.d.ts +10 -0
- package/dist/shared/safe-remove.js +66 -0
- package/dist/shared/schema.d.ts +527 -0
- package/dist/shared/schema.js +90 -0
- package/dist/shared/system.d.ts +15 -0
- package/dist/shared/system.js +19 -0
- package/dist/shared/test-guard.d.ts +4 -0
- package/dist/shared/test-guard.js +37 -0
- package/dist/sync/claude-mem.d.ts +8 -0
- package/dist/sync/claude-mem.js +79 -0
- package/dist/sync/history.d.ts +41 -0
- package/dist/sync/history.js +161 -0
- package/dist/sync/watcher.d.ts +21 -0
- package/dist/sync/watcher.js +135 -0
- package/hooks/post-agent/README.txt +1 -0
- package/hooks/pre-provision/README.txt +1 -0
- package/package.json +91 -4
- package/sandbox/bootstrap.ps1 +258 -0
- package/sandbox/cpucap.ps1 +51 -0
- package/sandbox/gitshim.cs +35 -0
- package/sandbox/logon.cmd +18 -0
- package/sandbox/openssl.cs +42 -0
- package/sandbox/ptyhost.cs +614 -0
- package/sandbox/trust.cmd +35 -0
- package/sandbox/vswhere.cs +83 -0
- package/schemas/config.schema.json +1161 -0
- package/schemas/sandbox-config.schema.json +968 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Live: the real session daemon + ptyhost with two projects at once. The launch folder (A) and a project in a root
|
|
2
|
+
// (B) each get two sessions requested at the same moment; each runs in its own folder with its own environment; B is
|
|
3
|
+
// set up once for both; viewers attach, detach and reattach, two at a time; exec and restart follow the project.
|
|
4
|
+
// Toolchain installers run as a dry run (no downloads); everything else is real. cmd.exe stands in for the agent.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { projectSpec } from '../host/join.js';
|
|
8
|
+
import { resizeFrame } from '../shared/pty-client.js';
|
|
9
|
+
import { readJsonFile } from '../shared/rundir.js';
|
|
10
|
+
import { copyCmdAsFakeAgent, openViewer, sees, startDaemon, tail, viewersWhen } from './live.js';
|
|
11
|
+
export const liveProjectsSuite = {
|
|
12
|
+
name: 'live-projects',
|
|
13
|
+
title: 'live: two projects, several sessions, several viewers',
|
|
14
|
+
live: true,
|
|
15
|
+
timeoutMs: 480000,
|
|
16
|
+
run: async (t, f) => {
|
|
17
|
+
const gen = await f.launch();
|
|
18
|
+
const data = t.at('live-multi-data');
|
|
19
|
+
const root = t.dir('live-root');
|
|
20
|
+
const projB = t.dir('live-root', 'projB');
|
|
21
|
+
copyCmdAsFakeAgent(data);
|
|
22
|
+
const session = f.readJson(gen.sessionPath);
|
|
23
|
+
// B's root appears at its own path here (like guestWorkspace "mirror"), so host and guest paths are the same.
|
|
24
|
+
Object.assign(session, { workspace: f.workA, attach: { enabled: true }, projectsDir: t.at('live-projects'), roots: [root], projectId: 'work-a' });
|
|
25
|
+
Object.assign(session.harness, { arguments: ['/d', '/k', 'echo AGENT-READY'], resumeArguments: ['/d', '/k', 'echo AGENT-RESUMED'] });
|
|
26
|
+
t.writeJson(gen.sessionPath, session);
|
|
27
|
+
const local = t.at('live-multi-local');
|
|
28
|
+
const daemon = startDaemon(t, f, { launchPath: gen.launchPath, data, workspace: f.workA, env: { AGENT_LOCAL_DIR: local, AGENT_DRY_RUN: '1', AGENT_MANAGED_DIR: t.at('live-managed') } });
|
|
29
|
+
const viewers = [];
|
|
30
|
+
const view = async (attach, cols = 100) => {
|
|
31
|
+
const v = await openViewer(attach, { cols });
|
|
32
|
+
viewers.push(v);
|
|
33
|
+
return v;
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
t.section('live: four sessions at once');
|
|
37
|
+
const cfg = structuredClone(f.baseConfig);
|
|
38
|
+
cfg.toolchains.environment = { LIVE_B: 'b-env' };
|
|
39
|
+
const spec = projectSpec(await f.run({ cwd: projB, trustConfig: true }, cfg), { name: 'projB', host: projB, guest: projB, primary: false, mechanism: 'root', registeredAt: '' }, false);
|
|
40
|
+
// All four at the same moment: two in the launch folder, two in B (whose setup they share).
|
|
41
|
+
daemon.request({ kind: 'session', id: 'a1', harness: 'claude', args: [], cwd: f.workA });
|
|
42
|
+
daemon.request({ kind: 'session', id: 'b1', harness: 'claude', args: [], cwd: projB, project: spec });
|
|
43
|
+
daemon.request({ kind: 'session', id: 'b2', harness: 'claude', args: [], cwd: projB, project: spec });
|
|
44
|
+
daemon.request({ kind: 'session', id: 'a2', harness: 'claude', args: [], cwd: f.workA });
|
|
45
|
+
const [a1, a2, b1, b2] = await Promise.all(['a1', 'a2', 'b1', 'b2'].map((id) => daemon.started(id)));
|
|
46
|
+
const all = [a1, a2, b1, b2];
|
|
47
|
+
const notRunning = all.filter((s) => s?.phase !== 'running' || !s.attach);
|
|
48
|
+
t.check(notRunning.length === 0, 'four sessions requested at once all start (two per project)', notRunning.map((s) => s?.id + ': ' + s?.phase + ' ' + s?.message).join('\n') + '\n' + daemon.diagnostics());
|
|
49
|
+
if (notRunning.length > 0)
|
|
50
|
+
return;
|
|
51
|
+
t.equal([a1.project, b1.project, b2.project], ['work-a', 'projB', 'projB'], 'each session knows its project');
|
|
52
|
+
const log = fs.readdirSync(path.join(data, 'logs')).filter((n) => n.startsWith('daemon')).map((n) => fs.readFileSync(path.join(data, 'logs', n), 'utf8')).join('\n');
|
|
53
|
+
t.equal((log.match(/Project projB: setting up/g) ?? []).length, 1, 'two sessions for the same new project at once share one setup (no second provisioning run)');
|
|
54
|
+
t.equal(readJsonFile(path.join(daemon.run, 'projects', 'projB.json'))?.phase, 'ready', 'the project\'s setup status is written for the host (sandbox status)');
|
|
55
|
+
t.section('live: each session has its own folder and environment');
|
|
56
|
+
const views = await Promise.all(all.map((s) => view(s.attach)));
|
|
57
|
+
for (const v of views)
|
|
58
|
+
await sees(t, v, 'AGENT-READY');
|
|
59
|
+
for (const v of views)
|
|
60
|
+
v.type('echo P=%AGENT_PROJECT% W=%AGENT_WORKSPACE% B=[%LIVE_B%]');
|
|
61
|
+
const bSees = await sees(t, views[2], 'P=projB W=' + projB + ' B=[b-env]');
|
|
62
|
+
t.check(Boolean(bSees), 'B\'s first session runs with B\'s workspace, project and variables', tail(views[2].output()));
|
|
63
|
+
t.check(Boolean(await sees(t, views[3], 'B=[b-env]')), 'B\'s second session runs with B\'s variables', tail(views[3].output()));
|
|
64
|
+
// (The daemon here runs without the boot's environment, so A's own AGENT_* variables are not set at all.)
|
|
65
|
+
t.check(Boolean(await sees(t, views[0], 'B=[%LIVE_B%]')), 'A\'s first session is untouched by B (B\'s variables never reach it)', tail(views[0].output()));
|
|
66
|
+
t.check(Boolean(await sees(t, views[1], 'B=[%LIVE_B%]')), 'A\'s second session is untouched by B', tail(views[1].output()));
|
|
67
|
+
views[2].type('echo CWD=%CD%.');
|
|
68
|
+
t.check(Boolean(await sees(t, views[2], 'CWD=' + projB + '.')), 'B\'s session starts in B\'s folder', tail(views[2].output()));
|
|
69
|
+
t.section('live: several viewers on one session');
|
|
70
|
+
const second = await view(b1.attach, 60);
|
|
71
|
+
const two = await viewersWhen(t, b1.attach, (v) => v.viewers === 2);
|
|
72
|
+
t.check(two !== null, 'a second viewer attaches to the same session');
|
|
73
|
+
second.socket.write(resizeFrame(90, 25));
|
|
74
|
+
views[2].type('echo TWO-VIEWERS-OK');
|
|
75
|
+
t.check(Boolean(await sees(t, views[2], 'TWO-VIEWERS-OK')), 'the first viewer sees what it typed (after the second one resized)', tail(views[2].output()));
|
|
76
|
+
t.check(Boolean(await sees(t, second, 'TWO-VIEWERS-OK')), 'the second viewer sees it too', tail(second.output()));
|
|
77
|
+
second.type('echo FROM-SECOND');
|
|
78
|
+
t.check(Boolean(await sees(t, views[2], 'FROM-SECOND')), 'input from the second viewer reaches the agent and the first viewer', tail(views[2].output()));
|
|
79
|
+
views[2].close();
|
|
80
|
+
second.close();
|
|
81
|
+
const none = await viewersWhen(t, b1.attach, (v) => v.viewers === 0);
|
|
82
|
+
t.check(none !== null, 'closing every viewer is noticed');
|
|
83
|
+
t.equal(daemon.status('b1')?.phase, 'running', 'closing every viewer never ends the session');
|
|
84
|
+
for (let round = 1; round <= 3; round++) {
|
|
85
|
+
const again = await view(b1.attach);
|
|
86
|
+
again.type('echo REATTACHED-' + round);
|
|
87
|
+
t.check(Boolean(await sees(t, again, 'REATTACHED-' + round)), 'reattach ' + round + ': a new viewer works on the same session', tail(again.output()));
|
|
88
|
+
again.close();
|
|
89
|
+
}
|
|
90
|
+
t.section('live: exec, restart and refusals follow the projects');
|
|
91
|
+
daemon.request({ kind: 'exec', id: 'xb', command: ['echo', '%LIVE_B%'], cwd: projB, timeoutSeconds: 30 });
|
|
92
|
+
daemon.request({ kind: 'exec', id: 'xa', command: ['echo', '[%LIVE_B%]'], cwd: f.workA, timeoutSeconds: 30 });
|
|
93
|
+
const [xb, xa] = await Promise.all([daemon.response('xb'), daemon.response('xa')]);
|
|
94
|
+
t.check(Boolean(xb?.stdout?.includes('b-env')), 'exec in B\'s folder gets B\'s environment', JSON.stringify(xb));
|
|
95
|
+
t.check(Boolean(xa?.stdout?.includes('[%LIVE_B%]')), 'exec in A\'s folder gets A\'s', JSON.stringify(xa));
|
|
96
|
+
daemon.request({ kind: 'session', id: 'bad1', harness: 'claude', args: [], cwd: f.workB, project: { ...spec, name: 'outside', workspace: f.workB } });
|
|
97
|
+
const bad = await daemon.started('bad1');
|
|
98
|
+
t.check(bad?.phase === 'failed' && bad.message.includes('not visible inside the sandbox'), 'a project outside the launch folder, the roots and C:\\Projects is refused', JSON.stringify(bad));
|
|
99
|
+
daemon.request({ kind: 'session', id: 'bad2', harness: 'claude', args: [], cwd: f.workA, project: spec });
|
|
100
|
+
const claimed = await daemon.started('bad2');
|
|
101
|
+
t.includes(claimed?.message ?? '', 'not inside project projB', 'a session cannot claim a project its folder is not in');
|
|
102
|
+
daemon.request({ kind: 'restart', id: 'rb', harness: null, cwd: projB });
|
|
103
|
+
t.equal((await daemon.response('rb'))?.message, 'restarted 2 session(s)', 'restart scoped to a project restarts only its sessions');
|
|
104
|
+
t.check(fs.existsSync(path.join(local, 'projects', 'projB', 'shell.cmd')), 'sbx-shell projB has a shell script');
|
|
105
|
+
t.includes(fs.readFileSync(path.join(local, 'projects', 'projB', 'env.cmd'), 'utf8'), 'LIVE_B=b-env', 'sbx-shell projB opens with B\'s environment');
|
|
106
|
+
t.check(fs.existsSync(path.join(local, 'bin', 'sbx-shell.cmd')), 'sbx-shell is on the sandbox\'s PATH');
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
for (const v of viewers)
|
|
110
|
+
v.close();
|
|
111
|
+
// End every agent: a fresh viewer types exit into each still-running session.
|
|
112
|
+
for (const id of ['a1', 'a2', 'b1', 'b2']) {
|
|
113
|
+
const s = daemon.status(id);
|
|
114
|
+
if (s?.phase !== 'running' || !s.attach)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const v = await openViewer(s.attach);
|
|
118
|
+
v.type('exit');
|
|
119
|
+
await t.waitFor(() => v.exitCode() !== null, 10000);
|
|
120
|
+
v.close();
|
|
121
|
+
}
|
|
122
|
+
catch { /* already gone */ }
|
|
123
|
+
}
|
|
124
|
+
await daemon.stop();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import type net from 'node:net';
|
|
3
|
+
import type { GuestResponse, SessionStatus } from '../core/types.js';
|
|
4
|
+
import { type Endpoint, type Viewers } from '../shared/pty-client.js';
|
|
5
|
+
import type { Fixtures } from './fixtures.js';
|
|
6
|
+
import type { Harness } from './harness.js';
|
|
7
|
+
export declare const CSC: string;
|
|
8
|
+
/** Live suites need Windows (ConPTY) and the .NET Framework compiler that ships with it. */
|
|
9
|
+
export declare const liveTestsAvailable: () => boolean;
|
|
10
|
+
/** Deadlines, in ms. Generous on purpose: they only matter when something is really wrong. */
|
|
11
|
+
export declare const WAIT: {
|
|
12
|
+
readonly start: 120000;
|
|
13
|
+
readonly screen: 45000;
|
|
14
|
+
readonly response: 60000;
|
|
15
|
+
readonly viewers: 45000;
|
|
16
|
+
readonly exit: 45000;
|
|
17
|
+
};
|
|
18
|
+
export declare const tail: (text: string, chars?: number) => string;
|
|
19
|
+
/** Connects, retrying until `ms`: a connection attempt can time out while the machine is busy. */
|
|
20
|
+
export declare const connectPatiently: (endpoint: Endpoint, ms?: number) => Promise<net.Socket>;
|
|
21
|
+
/** One connection to a session's ptyhost, collecting what the server sends. */
|
|
22
|
+
export interface Viewer {
|
|
23
|
+
socket: net.Socket;
|
|
24
|
+
/** Everything the agent has printed since this viewer connected. */
|
|
25
|
+
output: () => string;
|
|
26
|
+
/** The exit code frame, once the agent ended. */
|
|
27
|
+
exitCode: () => number | null;
|
|
28
|
+
/** The error frame ("access denied"), if the server sent one. */
|
|
29
|
+
error: () => string;
|
|
30
|
+
/** True once the server closed the connection. */
|
|
31
|
+
closed: () => boolean;
|
|
32
|
+
type: (text: string) => void;
|
|
33
|
+
close: () => void;
|
|
34
|
+
}
|
|
35
|
+
/** Connects like a terminal tab would (`token` can be wrong on purpose) and says hello. */
|
|
36
|
+
export declare const openViewer: (attach: Endpoint, options?: {
|
|
37
|
+
token?: string;
|
|
38
|
+
cols?: number;
|
|
39
|
+
rows?: number;
|
|
40
|
+
}) => Promise<Viewer>;
|
|
41
|
+
/** Polls until the viewer's screen shows `needle`. */
|
|
42
|
+
export declare const sees: (t: Harness, viewer: Viewer, needle: string, ms?: number) => Promise<true | null>;
|
|
43
|
+
/** Polls the session's viewer count until `accept` is satisfied; null when it never was. A query that gets no answer is retried. */
|
|
44
|
+
export declare const viewersWhen: (t: Harness, attach: Endpoint, accept: (v: Viewers) => boolean, ms?: number) => Promise<Viewers | null>;
|
|
45
|
+
export declare const copyCmdAsFakeAgent: (dataPath: string) => void;
|
|
46
|
+
/** A real session daemon (guest runtime) working on a generated launch folder, driven through the run folder like the host does. */
|
|
47
|
+
export interface LiveDaemon {
|
|
48
|
+
run: string;
|
|
49
|
+
child: ChildProcess;
|
|
50
|
+
request: (body: Record<string, unknown>) => void;
|
|
51
|
+
response: (id: string, ms?: number) => Promise<GuestResponse | null>;
|
|
52
|
+
status: (id: string) => SessionStatus | null;
|
|
53
|
+
/** Waits until a session has left "starting". */
|
|
54
|
+
started: (id: string, ms?: number) => Promise<SessionStatus | null>;
|
|
55
|
+
/** The daemon's stdout/stderr and its log files, for failure messages. */
|
|
56
|
+
diagnostics: () => string;
|
|
57
|
+
/** Ends the daemon. */
|
|
58
|
+
stop: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export declare const startDaemon: (t: Harness, f: Fixtures, options: {
|
|
61
|
+
launchPath: string;
|
|
62
|
+
data: string;
|
|
63
|
+
workspace: string;
|
|
64
|
+
env?: Record<string, string>;
|
|
65
|
+
}) => LiveDaemon;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Helpers for the live suites: they run the real session daemon, ptyhost (ConPTY) and the compiled git shim on Windows.
|
|
2
|
+
//
|
|
3
|
+
// Timing rule: a live test never sleeps for a fixed time and never gives up early. It polls until the condition holds,
|
|
4
|
+
// with a generous deadline (a busy machine, a cold csc.exe or an antivirus scan can make any step 10x slower), and a
|
|
5
|
+
// failure prints what was seen (the viewer's screen, the daemon's log) so the cause is visible.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { ensureDir, sleep } from '../core/util.js';
|
|
10
|
+
import { connect, encodeFrame, FrameReader, FrameType, helloFrame, queryViewers } from '../shared/pty-client.js';
|
|
11
|
+
import { readJsonFile, writeJsonAtomic } from '../shared/rundir.js';
|
|
12
|
+
const WINDIR = process.env.WINDIR ?? 'C:\\Windows';
|
|
13
|
+
export const CSC = path.join(WINDIR, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
|
|
14
|
+
/** Live suites need Windows (ConPTY) and the .NET Framework compiler that ships with it. */
|
|
15
|
+
export const liveTestsAvailable = () => process.platform === 'win32' && fs.existsSync(CSC);
|
|
16
|
+
/** Deadlines, in ms. Generous on purpose: they only matter when something is really wrong. */
|
|
17
|
+
export const WAIT = { start: 120000, screen: 45000, response: 60000, viewers: 45000, exit: 45000 };
|
|
18
|
+
export const tail = (text, chars = 500) => (text.length > chars ? '...' + text.slice(-chars) : text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
19
|
+
/** Connects, retrying until `ms`: a connection attempt can time out while the machine is busy. */
|
|
20
|
+
export const connectPatiently = async (endpoint, ms = 60000) => {
|
|
21
|
+
const end = Date.now() + ms;
|
|
22
|
+
for (;;) {
|
|
23
|
+
try {
|
|
24
|
+
return await connect(endpoint, 10000);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (Date.now() >= end)
|
|
28
|
+
throw e;
|
|
29
|
+
await sleep(250);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Connects like a terminal tab would (`token` can be wrong on purpose) and says hello. */
|
|
34
|
+
export const openViewer = async (attach, options = {}) => {
|
|
35
|
+
const socket = await connectPatiently({ ...attach, addresses: ['127.0.0.1'] });
|
|
36
|
+
const reader = new FrameReader();
|
|
37
|
+
let output = '';
|
|
38
|
+
let exitCode = null;
|
|
39
|
+
let error = '';
|
|
40
|
+
let closed = false;
|
|
41
|
+
socket.on('data', (chunk) => {
|
|
42
|
+
for (const frame of reader.push(chunk)) {
|
|
43
|
+
if (frame.type === FrameType.Output)
|
|
44
|
+
output += frame.payload.toString();
|
|
45
|
+
else if (frame.type === FrameType.Exit)
|
|
46
|
+
exitCode = frame.payload.readInt32LE(0);
|
|
47
|
+
else if (frame.type === FrameType.Error)
|
|
48
|
+
error = frame.payload.toString();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
socket.on('error', () => undefined);
|
|
52
|
+
socket.on('close', () => { closed = true; });
|
|
53
|
+
socket.write(helloFrame(options.token ?? attach.token, options.cols ?? 100, options.rows ?? 30));
|
|
54
|
+
return {
|
|
55
|
+
socket,
|
|
56
|
+
output: () => output,
|
|
57
|
+
exitCode: () => exitCode,
|
|
58
|
+
error: () => error,
|
|
59
|
+
closed: () => closed,
|
|
60
|
+
type: (text) => { socket.write(encodeFrame(FrameType.Input, Buffer.from(text + '\r'))); },
|
|
61
|
+
close: () => socket.destroy(),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Polls until the viewer's screen shows `needle`. */
|
|
65
|
+
export const sees = (t, viewer, needle, ms = WAIT.screen) => t.waitFor(() => viewer.output().includes(needle), ms);
|
|
66
|
+
/** Polls the session's viewer count until `accept` is satisfied; null when it never was. A query that gets no answer is retried. */
|
|
67
|
+
export const viewersWhen = (t, attach, accept, ms = WAIT.viewers) => t.waitFor(async () => {
|
|
68
|
+
const v = await queryViewers({ ...attach, addresses: ['127.0.0.1'] }, 5000);
|
|
69
|
+
return v && accept(v) ? v : null;
|
|
70
|
+
}, ms);
|
|
71
|
+
export const copyCmdAsFakeAgent = (dataPath) => {
|
|
72
|
+
const agentDir = path.join(dataPath, 'tools', 'agents', 'claude');
|
|
73
|
+
ensureDir(agentDir);
|
|
74
|
+
fs.copyFileSync(path.join(WINDIR, 'System32', 'cmd.exe'), path.join(agentDir, 'claude.exe'));
|
|
75
|
+
};
|
|
76
|
+
export const startDaemon = (t, f, options) => {
|
|
77
|
+
const { launchPath, data } = options;
|
|
78
|
+
const output = t.at(path.basename(data) + '-daemon.out');
|
|
79
|
+
const fd = fs.openSync(output, 'w');
|
|
80
|
+
const env = { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_WORKSPACE: options.workspace, AGENT_LOCAL_DIR: f.at(path.basename(data) + '-local'), AGENT_LOG_PREFIX: 'daemon', AGENT_NO_TABS: '1', ...options.env };
|
|
81
|
+
const child = spawn(process.execPath, [path.join(launchPath, 'runtime', 'daemon.js')], { env, stdio: ['ignore', fd, fd], windowsHide: true });
|
|
82
|
+
let exited = false;
|
|
83
|
+
child.on('exit', () => { exited = true; });
|
|
84
|
+
const run = path.join(data, 'run');
|
|
85
|
+
const status = (id) => readJsonFile(path.join(run, 'sessions', id + '.json'));
|
|
86
|
+
return {
|
|
87
|
+
run,
|
|
88
|
+
child,
|
|
89
|
+
request: (body) => writeJsonAtomic(path.join(run, 'requests', body.id + '.json'), { createdAt: new Date().toISOString(), ...body }),
|
|
90
|
+
response: (id, ms = WAIT.response) => t.waitFor(() => readJsonFile(path.join(run, 'responses', id + '.json')), ms),
|
|
91
|
+
status,
|
|
92
|
+
started: (id, ms = WAIT.start) => t.waitFor(() => { const s = status(id); return s && s.phase !== 'starting' ? s : null; }, ms),
|
|
93
|
+
diagnostics: () => {
|
|
94
|
+
const logs = path.join(data, 'logs');
|
|
95
|
+
const files = fs.existsSync(logs) ? fs.readdirSync(logs).filter((n) => n.startsWith('daemon')) : [];
|
|
96
|
+
const logText = files.map((n) => fs.readFileSync(path.join(logs, n), 'utf8')).join('\n');
|
|
97
|
+
return 'daemon output: ' + tail(fs.readFileSync(output, 'utf8'), 600) + '\ndaemon log: ' + tail(logText, 1200);
|
|
98
|
+
},
|
|
99
|
+
stop: async () => {
|
|
100
|
+
if (!exited)
|
|
101
|
+
child.kill();
|
|
102
|
+
await t.waitFor(() => exited, 10000);
|
|
103
|
+
try {
|
|
104
|
+
fs.closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
catch { /* already closed */ }
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Guest paths on other drive letters (C:\Mirror\<letter> + subst) and guestWorkspace "mirror".
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Config, Mapping } from '../core/schema.js';
|
|
5
|
+
import { guestPathFor } from '../cli/commands/context.js';
|
|
6
|
+
import { buildWsb } from '../host/launch/wsb.js';
|
|
7
|
+
import { guestDrive, isValidGuestPath, planMirrorWorkspace, sandboxFolder, substDrives } from '../shared/guest-paths.js';
|
|
8
|
+
import { guestWorkspaceAliases, projectFolderName } from '../shared/paths.js';
|
|
9
|
+
import { syncHistoryIn, syncHistoryOut } from '../sync/history.js';
|
|
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
|
+
},
|
|
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
|
+
};
|