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,219 @@
|
|
|
1
|
+
// Zero-click protection for the folder the sandbox shares with this PC.
|
|
2
|
+
//
|
|
3
|
+
// The agent can write the workspace. Two kinds of change there would run code on THIS PC without you doing anything
|
|
4
|
+
// unusual, so they are snapshotted before launch, re-checked every minute while the sandbox runs and when it closes,
|
|
5
|
+
// and neutralised:
|
|
6
|
+
// - git: new or changed hooks, and config that makes git run programs (core.fsmonitor, core.sshCommand, filters,
|
|
7
|
+
// include.path, ...), plus files that redirect where git reads config and hooks (commondir, config.worktree);
|
|
8
|
+
// - commands: executables dropped in the project root under the name of something you type there (node.exe,
|
|
9
|
+
// git.cmd, npm.cmd, ...), because Windows runs a program from the current folder before searching PATH.
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import _ from 'lodash';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { readValid, writeFileAtomic, writeJsonAtomic } from '../shared/rundir.js';
|
|
16
|
+
import { assertTestWritable } from './test-guard.js';
|
|
17
|
+
import { dirExists, fileExists, isLink } from '../core/util.js';
|
|
18
|
+
/** Config keys that make git run a program, or load more config. */
|
|
19
|
+
const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool)\s*=/i;
|
|
20
|
+
const SECTION = /^\s*\[([^\]]*)\]\s*/;
|
|
21
|
+
const MARK = '# [agent-sandbox] disabled, added inside the sandbox: ';
|
|
22
|
+
/** Files in .git that make git read config/hooks from somewhere else. */
|
|
23
|
+
const REDIRECTS = ['commondir', 'config.worktree'];
|
|
24
|
+
/** Names you are likely to run in a project folder; with any of these extensions Windows would pick the local copy. */
|
|
25
|
+
const COMMANDS = new RegExp('^(' + [
|
|
26
|
+
// this tool's own commands (package.json "bin")
|
|
27
|
+
'sandbox', 'windows-development-sandbox', 'agent-sandbox', 'claude-sbx', 'claude-sandbox', 'codex-sbx', 'codex-sandbox', 'gemini-sbx', 'gemini-sandbox', 'claude-host', 'sbx-show', 'sbx-hide',
|
|
28
|
+
// agents
|
|
29
|
+
'claude', 'codex', 'gemini', 'opencode', 'qwen', 'crush', 'copilot', 'kilo', 'kilocode', 'amp', 'cline', 'cn', 'kimi', 'reasonix', 'codewhale', 'aider', 'cursor-agent', 'agent', 'goose',
|
|
30
|
+
// toolchains and everyday tools
|
|
31
|
+
'node', 'npm', 'npx', 'pnpm', 'pnpx', 'yarn', 'corepack', 'volta', 'nvm', 'deno', 'bun', 'bunx', 'tsc', 'git', 'gh', 'code', 'python', 'python3', 'py', 'pip', 'pip3', 'uv', 'uvx', 'pyenv', 'poetry',
|
|
32
|
+
'cargo', 'rustc', 'rustup', 'dotnet', 'nuget', 'msbuild', 'go', 'java', 'javac', 'mvn', 'mvnw', 'gradle', 'gradlew', 'ruby', 'gem', 'bundle', 'rake', 'rails', 'php', 'composer',
|
|
33
|
+
'make', 'cmake', 'ninja', 'gcc', 'g\\+\\+', 'clang', 'psql', 'mysql', 'mariadb', 'mongosh', 'redis-cli', 'docker', 'kubectl', 'terraform', 'az', 'aws',
|
|
34
|
+
'powershell', 'pwsh', 'cmd', 'where', 'tasklist', 'tar', 'curl', 'ssh', 'scp',
|
|
35
|
+
].join('|') + ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
|
|
36
|
+
/** SHA-256 in small chunks: a sandbox can plant a multi-gigabyte "node.exe" to exhaust this PC's memory. */
|
|
37
|
+
const hash = (file) => {
|
|
38
|
+
if (isLink(file))
|
|
39
|
+
return 'link';
|
|
40
|
+
const sum = crypto.createHash('sha256');
|
|
41
|
+
const buffer = Buffer.alloc(1 << 20);
|
|
42
|
+
let fd;
|
|
43
|
+
// A file that cannot be read (locked by a program in the sandbox) never matches the snapshot, so it is dealt with.
|
|
44
|
+
try {
|
|
45
|
+
fd = fs.openSync(file, 'r');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'unreadable-' + crypto.randomBytes(4).toString('hex');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
for (let n = fs.readSync(fd, buffer, 0, buffer.length, null); n > 0; n = fs.readSync(fd, buffer, 0, buffer.length, null))
|
|
52
|
+
sum.update(buffer.subarray(0, n));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
return sum.digest('hex');
|
|
58
|
+
};
|
|
59
|
+
const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
|
|
60
|
+
const plainGitDir = (workspace) => {
|
|
61
|
+
const dir = path.join(workspace, '.git');
|
|
62
|
+
return dirExists(dir) && !isLink(dir) ? dir : null;
|
|
63
|
+
};
|
|
64
|
+
const filesIn = (dir, accept) => dirExists(dir) && !isLink(dir)
|
|
65
|
+
? Object.fromEntries(fs.readdirSync(dir, { withFileTypes: true }).filter((e) => (e.isFile() || e.isSymbolicLink()) && accept(e.name)).map((e) => [e.name, hash(path.join(dir, e.name))]))
|
|
66
|
+
: {};
|
|
67
|
+
const configLines = (dir) => {
|
|
68
|
+
const file = path.join(dir, 'config');
|
|
69
|
+
return fileExists(file) && !isLink(file) && fs.statSync(file).size <= MAX_CONFIG_BYTES ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
|
|
70
|
+
};
|
|
71
|
+
export const snapshotRepo = (workspace) => {
|
|
72
|
+
const dir = plainGitDir(workspace);
|
|
73
|
+
return {
|
|
74
|
+
gitDir: dir !== null,
|
|
75
|
+
config: dir ? configLines(dir) : [],
|
|
76
|
+
hooks: dir ? filesIn(path.join(dir, 'hooks'), (n) => !n.endsWith('.sample')) : {},
|
|
77
|
+
redirects: dir ? REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n))) : [],
|
|
78
|
+
commands: filesIn(workspace, (n) => COMMANDS.test(n)),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/** Moves files into <dir>\agent-sandbox-quarantine\<time>\ and reports them. */
|
|
82
|
+
const quarantine = (from, names, into, kind) => {
|
|
83
|
+
if (names.length === 0)
|
|
84
|
+
return [];
|
|
85
|
+
const target = path.join(into, 'agent-sandbox-quarantine', String(Date.now()));
|
|
86
|
+
fs.mkdirSync(target, { recursive: true });
|
|
87
|
+
return names.map((name) => {
|
|
88
|
+
try {
|
|
89
|
+
fs.renameSync(path.join(from, name), path.join(target, name));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return { kind, detail: name + ' (could not be moved: ' + e.code + '; delete it yourself before running anything here)' };
|
|
93
|
+
}
|
|
94
|
+
return { kind, detail: name + ' (moved to ' + target + ')' };
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
/** Comments out newly added config lines that make git run programs. Existing lines are never touched. */
|
|
98
|
+
const disableConfig = (dir, before) => {
|
|
99
|
+
const lines = configLines(dir);
|
|
100
|
+
const known = _.countBy(before);
|
|
101
|
+
const seen = {};
|
|
102
|
+
const findings = [];
|
|
103
|
+
let inInclude = false;
|
|
104
|
+
const fixed = lines.map((line) => {
|
|
105
|
+
const header = SECTION.exec(line);
|
|
106
|
+
if (header)
|
|
107
|
+
inInclude = /^include(if\s+"[^"]*")?$/i.test(header[1].trim());
|
|
108
|
+
seen[line] = (seen[line] ?? 0) + 1;
|
|
109
|
+
if (line.startsWith(MARK) || seen[line] <= (known[line] ?? 0))
|
|
110
|
+
return line;
|
|
111
|
+
// git also accepts "[core] fsmonitor = x" on one line, so look past a leading section header.
|
|
112
|
+
const keyPart = line.replace(SECTION, '').trim();
|
|
113
|
+
if (!(DANGEROUS_KEY.test(keyPart) || (inInclude && keyPart.includes('='))))
|
|
114
|
+
return line;
|
|
115
|
+
findings.push({ kind: 'config', detail: line.trim() });
|
|
116
|
+
return MARK + line.trim();
|
|
117
|
+
});
|
|
118
|
+
if (findings.length)
|
|
119
|
+
writeFileAtomic(path.join(dir, 'config'), fixed.join('\n'));
|
|
120
|
+
const file = path.join(dir, 'config');
|
|
121
|
+
// A link or a huge file is not read at all, so say so instead of letting it pass as "nothing found".
|
|
122
|
+
const unchecked = isLink(file) ? '.git/config is a link' : lines.length === 0 && fileExists(file) && fs.statSync(file).size > MAX_CONFIG_BYTES ? '.git/config is over 4 MB' : null;
|
|
123
|
+
if (unchecked)
|
|
124
|
+
findings.push({ kind: 'git', detail: unchecked + ', so it cannot be checked. Do not run git in this folder until you have looked at it.' });
|
|
125
|
+
return findings;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Compares the workspace with its pre-launch snapshot and neutralises anything new that would execute on this PC.
|
|
129
|
+
* Returns what was found (and fixed). Never touches what was already there before the sandbox ran.
|
|
130
|
+
*/
|
|
131
|
+
export const guardRepo = (workspace, before) => {
|
|
132
|
+
assertTestWritable(workspace);
|
|
133
|
+
const findings = [];
|
|
134
|
+
const commandsNow = filesIn(workspace, (n) => COMMANDS.test(n));
|
|
135
|
+
findings.push(...quarantine(workspace, Object.keys(commandsNow).filter((n) => before.commands[n] !== commandsNow[n]), workspace, 'command'));
|
|
136
|
+
const dir = plainGitDir(workspace);
|
|
137
|
+
if (!dir) {
|
|
138
|
+
const replaced = before.gitDir && fs.existsSync(path.join(workspace, '.git'));
|
|
139
|
+
if (replaced)
|
|
140
|
+
findings.push({ kind: 'git', detail: '.git was replaced by a link or a "gitdir:" file inside the sandbox. Do not run git in ' + workspace + ' until you have checked it.' });
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
const hooksDir = path.join(dir, 'hooks');
|
|
144
|
+
const hooksNow = filesIn(hooksDir, (n) => !n.endsWith('.sample'));
|
|
145
|
+
findings.push(...quarantine(hooksDir, Object.keys(hooksNow).filter((n) => before.hooks[n] !== hooksNow[n]), dir, 'hook'));
|
|
146
|
+
findings.push(...quarantine(dir, REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n)) && !before.redirects.includes(n)), dir, 'git'));
|
|
147
|
+
findings.push(...disableConfig(dir, before.config));
|
|
148
|
+
return findings;
|
|
149
|
+
};
|
|
150
|
+
// ---- snapshot storage (host-only, next to the sync state) ---------------------------------------------------------
|
|
151
|
+
const syncDir = (dataPath) => path.join(path.dirname(path.dirname(dataPath)), 'sync');
|
|
152
|
+
const snapshotFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo.json');
|
|
153
|
+
const findingsFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo-findings.json');
|
|
154
|
+
const Finding = z.object({ kind: z.enum(['hook', 'config', 'git', 'command']), detail: z.string().max(4096), at: z.string().max(64).optional() });
|
|
155
|
+
const Saved = z.object({
|
|
156
|
+
workspace: z.string().max(1024).refine((p) => path.isAbsolute(p)),
|
|
157
|
+
snapshot: z.object({ gitDir: z.boolean(), config: z.array(z.string()), hooks: z.record(z.string(), z.string()), redirects: z.array(z.string()), commands: z.record(z.string(), z.string()) }),
|
|
158
|
+
});
|
|
159
|
+
const Store = z.strictObject({ projects: z.array(Saved).max(4096), roots: z.array(z.string().max(1024).refine((p) => path.isAbsolute(p))).max(64) });
|
|
160
|
+
const StoreAny = z.union([Store, Saved.transform((s) => ({ projects: [s], roots: [] }))]);
|
|
161
|
+
const readStore = (dataPath) => readValid(StoreAny, snapshotFile(dataPath)) ?? { projects: [], roots: [] };
|
|
162
|
+
const EMPTY = { gitDir: false, config: [], hooks: {}, redirects: [], commands: {} };
|
|
163
|
+
const MAX_ROOT_PROJECTS = 2000;
|
|
164
|
+
/** The folders directly inside a project root (plain folders only; links are never followed). */
|
|
165
|
+
const rootProjects = (root) => {
|
|
166
|
+
if (!dirExists(root) || isLink(root))
|
|
167
|
+
return [];
|
|
168
|
+
return fs.readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink()).slice(0, MAX_ROOT_PROJECTS).map((e) => path.join(root, e.name));
|
|
169
|
+
};
|
|
170
|
+
const has = (projects, workspace) => projects.some((s) => s.workspace.toLowerCase() === workspace.toLowerCase());
|
|
171
|
+
/**
|
|
172
|
+
* At launch: every folder the sandbox will be able to write. The launch folder, projects mapped with it and every
|
|
173
|
+
* project already inside a project root are snapshotted as they are now, before the sandbox can touch them.
|
|
174
|
+
*/
|
|
175
|
+
export const saveRepoSnapshot = (dataPath, workspace, more = [], roots = []) => {
|
|
176
|
+
const folders = _.uniqBy([workspace, ...more, ...roots.flatMap((r) => [r, ...rootProjects(r)])], (w) => w.toLowerCase());
|
|
177
|
+
writeJsonAtomic(snapshotFile(dataPath), { projects: folders.map((w) => ({ workspace: w, snapshot: snapshotRepo(w) })), roots });
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* A project that joins the running sandbox: snapshotted before its first session, then guarded like the launch
|
|
181
|
+
* folder. A project already guarded keeps its first snapshot (what was there before the sandbox could touch it).
|
|
182
|
+
*/
|
|
183
|
+
export const addRepoSnapshot = (dataPath, workspace, o = {}) => {
|
|
184
|
+
const store = readStore(dataPath);
|
|
185
|
+
// A folder inside one that is guarded already is covered by it only for its root files; it gets its own entry.
|
|
186
|
+
if (has(store.projects, workspace))
|
|
187
|
+
return;
|
|
188
|
+
writeJsonAtomic(snapshotFile(dataPath), { ...store, projects: [...store.projects, { workspace, snapshot: o.empty ? EMPTY : snapshotRepo(workspace) }] });
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Folders that appeared inside a project root after the launch: the sandbox could have written them from the start,
|
|
192
|
+
* so they get an EMPTY baseline: any git hook, git config that runs programs (core.hooksPath, core.fsmonitor, ...)
|
|
193
|
+
* or planted command in them counts as added inside the sandbox and is neutralised.
|
|
194
|
+
*/
|
|
195
|
+
const discoverRootProjects = (dataPath) => {
|
|
196
|
+
const store = readStore(dataPath);
|
|
197
|
+
const found = store.roots.flatMap(rootProjects).filter((w) => !has(store.projects, w)).map((workspace) => ({ workspace, snapshot: EMPTY }));
|
|
198
|
+
if (!found.length)
|
|
199
|
+
return store.projects;
|
|
200
|
+
const projects = [...store.projects, ...found].slice(0, 4096);
|
|
201
|
+
writeJsonAtomic(snapshotFile(dataPath), { ...store, projects });
|
|
202
|
+
return projects;
|
|
203
|
+
};
|
|
204
|
+
/** The folders being guarded (for status). */
|
|
205
|
+
export const guardedProjects = (dataPath) => readStore(dataPath).projects.map((s) => s.workspace);
|
|
206
|
+
/** Re-checks every project of the current/last launch; findings are kept in a host-only file for `status`. Returns the new ones. */
|
|
207
|
+
export const checkRepo = (dataPath) => {
|
|
208
|
+
const saved = discoverRootProjects(dataPath);
|
|
209
|
+
if (!saved.length)
|
|
210
|
+
return [];
|
|
211
|
+
const old = repoFindings(dataPath);
|
|
212
|
+
// A finding that cannot be fixed (a file locked in the sandbox) is reported once, not every minute.
|
|
213
|
+
const findings = saved.flatMap((s) => guardRepo(s.workspace, s.snapshot).map((f) => (saved.length > 1 ? { ...f, detail: s.workspace + ': ' + f.detail } : f)))
|
|
214
|
+
.filter((f) => !old.some((o) => o.kind === f.kind && o.detail === f.detail));
|
|
215
|
+
if (findings.length)
|
|
216
|
+
writeJsonAtomic(findingsFile(dataPath), [...old, ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
|
|
217
|
+
return findings;
|
|
218
|
+
};
|
|
219
|
+
export const repoFindings = (dataPath) => readValid(z.array(Finding).max(10000), findingsFile(dataPath)) ?? [];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Config } from '../core/types.js';
|
|
2
|
+
export type SecretsConfig = Config['secrets'];
|
|
3
|
+
export declare const isSecretName: (name: string, secrets: SecretsConfig) => boolean;
|
|
4
|
+
export declare const isSecretValue: (value: string) => boolean;
|
|
5
|
+
/** Whether a variable (name and value) may go into the sandbox. secrets.allow always wins. */
|
|
6
|
+
export declare const mayPass: (name: string, value: unknown, secrets: SecretsConfig) => boolean;
|
|
7
|
+
/** A copy of an env/headers map without the secrets; the names removed are added to `removed` (with `where`). */
|
|
8
|
+
export declare const stripVariables: (vars: unknown, secrets: SecretsConfig, where: string, removed: string[]) => unknown;
|
|
9
|
+
/** settings.json for the sandbox: secrets out of "env", credential helper commands removed. */
|
|
10
|
+
export declare const stripSettings: (settings: Record<string, unknown>, secrets: SecretsConfig, removed: string[]) => Record<string, unknown>;
|
|
11
|
+
/** Every MCP server in a .claude.json-shaped object (top level and per project). */
|
|
12
|
+
export declare const stripMcpServers: (state: Record<string, unknown>, secrets: SecretsConfig, removed: string[]) => Record<string, unknown>;
|
|
13
|
+
/** Secret-looking files in the workspace root and one folder down (skipping dependency folders). */
|
|
14
|
+
export declare const findSecretFiles: (workspace: string) => string[];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Keeps credentials on this PC. Everything copied into the sandbox from your Claude settings (settings.json "env",
|
|
2
|
+
// MCP servers' env / headers / args in .claude.json) goes through here first: cloud and production credentials are
|
|
3
|
+
// removed unless you let them through with secrets.allow. Pure functions, so they are easy to test.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/** Variable names that are credentials by definition: cloud providers, hosting, CI, registries, payment, AI APIs, ... */
|
|
7
|
+
const SECRET_NAMES = [
|
|
8
|
+
/^AWS_/, /^AMAZON_/, /^AZURE_/, /^ARM_(CLIENT|TENANT|SUBSCRIPTION|ACCESS)_/, /^MSAL_/, /^CLOUDFLARE_/, /^CF_(API|ACCOUNT|ZONE|ACCESS)/,
|
|
9
|
+
/^GOOGLE_(APPLICATION_CREDENTIALS|API_KEY|CLOUD_|CLIENT_SECRET)/, /^GCP_/, /^GCLOUD_/, /^GOOGLE_GHA_/, /^FIREBASE_/,
|
|
10
|
+
/^DIGITALOCEAN_/, /^DO_(API|AUTH)_/, /^LINODE_/, /^HCLOUD_/, /^OCI_/, /^ALIBABA_CLOUD_/, /^ALICLOUD_/, /^IBMCLOUD_/,
|
|
11
|
+
/^HEROKU_/, /^VERCEL_/, /^NETLIFY_/, /^RENDER_API/, /^FLY_(API|ACCESS)_/, /^RAILWAY_/, /^SUPABASE_/, /^NEON_/, /^PLANETSCALE_/,
|
|
12
|
+
/^GITHUB_TOKEN$/, /^GH_TOKEN$/, /^GH_ENTERPRISE_TOKEN$/, /^GITHUB_PAT/, /^GITLAB_/, /^BITBUCKET_/, /^CIRCLE_TOKEN$/, /^TRAVIS_/, /^BUILDKITE_/,
|
|
13
|
+
/^NPM_TOKEN$/, /^NODE_AUTH_TOKEN$/, /^YARN_NPM_AUTH/, /^NUGET_(API_KEY|AUTH)/, /^PYPI_/, /^TWINE_/, /^CARGO_REGISTRY_TOKEN$/, /^GEM_HOST_API_KEY$/,
|
|
14
|
+
/^DOCKER_(PASSWORD|TOKEN|AUTH)/, /^REGISTRY_(PASSWORD|TOKEN)/, /^KUBECONFIG$/, /^KUBE_TOKEN$/, /^HELM_/, /^VAULT_/, /^CONSUL_HTTP_TOKEN$/, /^NOMAD_TOKEN$/,
|
|
15
|
+
/^TF_VAR_/, /^TF_TOKEN_/, /^TFE_TOKEN$/, /^PULUMI_ACCESS_TOKEN$/, /^SPACELIFT_/, /^ANSIBLE_VAULT/,
|
|
16
|
+
/^STRIPE_/, /^PAYPAL_/, /^BRAINTREE_/, /^SQUARE_/, /^ADYEN_/, /^TWILIO_/, /^SENDGRID_/, /^MAILGUN_/, /^POSTMARK_/, /^MAILCHIMP_/, /^RESEND_/,
|
|
17
|
+
/^SLACK_/, /^DISCORD_(TOKEN|BOT|WEBHOOK)/, /^TELEGRAM_BOT/, /^SENTRY_(AUTH|DSN)/, /^DATADOG_/, /^DD_(API|APP)_KEY$/, /^NEW_RELIC_/, /^HONEYCOMB_/,
|
|
18
|
+
/^OPENAI_/, /^ANTHROPIC_(API_KEY|AUTH_TOKEN)$/, /^CLAUDE_CODE_OAUTH_TOKEN$/, /^GEMINI_API_KEY$/, /^MISTRAL_API_KEY$/, /^COHERE_API_KEY$/,
|
|
19
|
+
/^GROQ_API_KEY$/, /^HF_TOKEN$/, /^HUGGING_?FACE_/, /^REPLICATE_API/, /^PINECONE_/, /^ELEVENLABS_/,
|
|
20
|
+
/^DATABASE_URL$/, /^.*_DATABASE_URL$/, /^(PG|POSTGRES|MYSQL|MARIADB|MSSQL|MONGO(DB)?|REDIS|ELASTIC(SEARCH)?|RABBITMQ|SNOWFLAKE|DATABRICKS)_/,
|
|
21
|
+
/^SSH_AUTH_SOCK$/, /^GPG_/, /^OP_(SERVICE_ACCOUNT|CONNECT)_/, /^BWS_ACCESS_TOKEN$/, /^DOPPLER_/, /^INFISICAL_/,
|
|
22
|
+
];
|
|
23
|
+
/** Any name that says what it holds: ..._TOKEN, ..._SECRET, PASSWORD, API_KEY, ..._PRIVATE_KEY, connection strings, ... */
|
|
24
|
+
const SECRET_WORDS = /(^|_)(PROD|PRODUCTION|LIVE|SECRETS?|TOKENS?|PASSWORDS?|PASSWD|PWD|PASS|API_?KEYS?|ACCESS_?KEYS?|PRIVATE_?KEYS?|SIGNING_?KEYS?|CREDENTIALS?|CREDS|AUTH|AUTHORIZATION|BEARER|COOKIES?|SESSION_?KEYS?|CONN(ECTION)?_?STR(ING)?|DSN|WEBHOOK(_URL)?|CLIENT_?SECRET|SAS|SALT|PAT)(_|$)/;
|
|
25
|
+
/** Values that are credentials whatever they are called. */
|
|
26
|
+
const SECRET_VALUES = [
|
|
27
|
+
/\b(AKIA|ASIA|AGPA|AIDA|AROA)[0-9A-Z]{16}\b/, // AWS access key ids
|
|
28
|
+
/\bgh[pousr]_[A-Za-z0-9]{30,}/, /\bgithub_pat_[A-Za-z0-9_]{40,}/, /\bglpat-[A-Za-z0-9_-]{20,}/,
|
|
29
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/, /\bsk-(ant-|proj-|live_|test_)?[A-Za-z0-9_-]{20,}/, /\b[rs]k_live_[A-Za-z0-9]{20,}/,
|
|
30
|
+
/\bAIza[0-9A-Za-z_-]{35}\b/, /\bya29\.[0-9A-Za-z_-]{20,}/, /\bnpm_[A-Za-z0-9]{36}\b/, /\bhf_[A-Za-z0-9]{30,}/,
|
|
31
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/, /AccountKey=[A-Za-z0-9+/=]{20,}/i, /SharedAccessSignature=|[?&]sig=[A-Za-z0-9%+/=]{20,}/i,
|
|
32
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s@/]+@/i, // user:password@host in any URL (database URLs, registries)
|
|
33
|
+
/\bBearer\s+\S{16,}/i, /\bBasic\s+[A-Za-z0-9+/=]{12,}/i,
|
|
34
|
+
/[?&](api[_-]?key|apikey|access[_-]?token|token|key|secret|password|sig|signature|auth)=[^&\s]{8,}/i, // credentials in a URL
|
|
35
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, // JWTs
|
|
36
|
+
];
|
|
37
|
+
/** "AWS_REGION", "MY_*", "*_URL": * matches anything. Case-insensitive, like Windows variable names. */
|
|
38
|
+
const globMatch = (pattern, name) => new RegExp('^' + pattern.split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', 'i').test(name);
|
|
39
|
+
export const isSecretName = (name, secrets) => {
|
|
40
|
+
// X-API-Key, x-auth-token, api.key: compared as X_API_KEY, X_AUTH_TOKEN, API_KEY.
|
|
41
|
+
const upper = name.toUpperCase().replace(/[-\s.]/g, '_');
|
|
42
|
+
return SECRET_NAMES.some((r) => r.test(upper)) || SECRET_WORDS.test(upper) || secrets.patterns.some((p) => globMatch(p, name));
|
|
43
|
+
};
|
|
44
|
+
export const isSecretValue = (value) => SECRET_VALUES.some((r) => r.test(value));
|
|
45
|
+
/** Whether a variable (name and value) may go into the sandbox. secrets.allow always wins. */
|
|
46
|
+
export const mayPass = (name, value, secrets) => {
|
|
47
|
+
if (!secrets.strip || secrets.allow.some((p) => globMatch(p, name)))
|
|
48
|
+
return true;
|
|
49
|
+
return !isSecretName(name, secrets) && !(typeof value === 'string' && isSecretValue(value));
|
|
50
|
+
};
|
|
51
|
+
/** A copy of an env/headers map without the secrets; the names removed are added to `removed` (with `where`). */
|
|
52
|
+
export const stripVariables = (vars, secrets, where, removed) => {
|
|
53
|
+
if (typeof vars !== 'object' || vars === null || Array.isArray(vars))
|
|
54
|
+
return vars;
|
|
55
|
+
return Object.fromEntries(Object.entries(vars).filter(([name, value]) => {
|
|
56
|
+
const ok = mayPass(name, value, secrets);
|
|
57
|
+
if (!ok)
|
|
58
|
+
removed.push(where + name);
|
|
59
|
+
return ok;
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
/** Claude settings keys that run commands to fetch credentials on this PC (they cannot work in the sandbox anyway). */
|
|
63
|
+
const CREDENTIAL_HELPERS = ['apiKeyHelper', 'awsAuthRefresh', 'awsCredentialExport', 'otelHeadersHelper'];
|
|
64
|
+
/** settings.json for the sandbox: secrets out of "env", credential helper commands removed. */
|
|
65
|
+
export const stripSettings = (settings, secrets, removed) => {
|
|
66
|
+
if (!secrets.strip)
|
|
67
|
+
return settings;
|
|
68
|
+
const out = { ...settings };
|
|
69
|
+
for (const key of CREDENTIAL_HELPERS.filter((k) => k in out)) {
|
|
70
|
+
delete out[key];
|
|
71
|
+
removed.push('settings.json ' + key);
|
|
72
|
+
}
|
|
73
|
+
if ('env' in out)
|
|
74
|
+
out.env = stripVariables(out.env, secrets, 'settings.json env.', removed);
|
|
75
|
+
return out;
|
|
76
|
+
};
|
|
77
|
+
/** One MCP server: secret env vars and headers removed, and secret-looking command-line arguments blanked. */
|
|
78
|
+
const stripServer = (server, secrets, where, removed) => {
|
|
79
|
+
if (typeof server !== 'object' || server === null)
|
|
80
|
+
return server;
|
|
81
|
+
const out = { ...server };
|
|
82
|
+
if ('env' in out)
|
|
83
|
+
out.env = stripVariables(out.env, secrets, where + ' env.', removed);
|
|
84
|
+
if ('headers' in out)
|
|
85
|
+
out.headers = stripVariables(out.headers, secrets, where + ' header ', removed);
|
|
86
|
+
if (typeof out.url === 'string' && secrets.strip && isSecretValue(out.url)) {
|
|
87
|
+
removed.push(where + ' url (it carried a credential)');
|
|
88
|
+
out.url = out.url.replace(/([?&])[^=&#]+=[^&#]*/g, (m, sep) => (isSecretValue('?' + m.slice(1)) ? sep + m.slice(1).split('=')[0] + '=REMOVED' : m));
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(out.args) && secrets.strip) {
|
|
91
|
+
out.args = out.args.map((arg, i) => {
|
|
92
|
+
const previous = i > 0 ? String(out.args[i - 1]) : '';
|
|
93
|
+
const [flag, inline] = typeof arg === 'string' && /^--?[\w-]+=/.test(arg) ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [null, null];
|
|
94
|
+
const secretFlag = (f) => /^--?[\w-]+$/.test(f) && isSecretName(f.replace(/^-+/, ''), secrets);
|
|
95
|
+
const flagged = typeof arg === 'string' && (isSecretValue(arg) || (flag !== null && (secretFlag(flag) || isSecretValue(inline))) || secretFlag(previous));
|
|
96
|
+
if (flagged)
|
|
97
|
+
removed.push(where + ' argument ' + (i + 1));
|
|
98
|
+
return flagged ? '[removed by agent-sandbox]' : arg;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
};
|
|
103
|
+
/** Every MCP server in a .claude.json-shaped object (top level and per project). */
|
|
104
|
+
export const stripMcpServers = (state, secrets, removed) => {
|
|
105
|
+
if (!secrets.strip)
|
|
106
|
+
return state;
|
|
107
|
+
const servers = (owner, where) => {
|
|
108
|
+
const list = owner.mcpServers;
|
|
109
|
+
if (typeof list !== 'object' || list === null)
|
|
110
|
+
return owner;
|
|
111
|
+
return { ...owner, mcpServers: Object.fromEntries(Object.entries(list).map(([name, s]) => [name, stripServer(s, secrets, where + 'MCP ' + name, removed)])) };
|
|
112
|
+
};
|
|
113
|
+
const top = servers(state, '');
|
|
114
|
+
const projects = top.projects;
|
|
115
|
+
return projects ? { ...top, projects: Object.fromEntries(Object.entries(projects).map(([key, p]) => [key, servers(p, '')])) } : top;
|
|
116
|
+
};
|
|
117
|
+
/** Files in the workspace that usually hold secrets. The agent CAN read them (the whole folder is mapped); we only warn. */
|
|
118
|
+
const SECRET_FILES = /^(\.env(\..+)?|\.envrc|.*\.(pem|key|p12|pfx|jks|keystore|kdbx|tfvars|ovpn)|credentials(\.json)?|secrets?\.(json|ya?ml|toml)|service[-_]?account.*\.json|id_(rsa|ed25519|ecdsa)|\.npmrc|\.pypirc|\.netrc|\.git-credentials)$/i;
|
|
119
|
+
const TEMPLATE = /\.(example|sample|template|dist|defaults?)$/i;
|
|
120
|
+
/** Secret-looking files in the workspace root and one folder down (skipping dependency folders). */
|
|
121
|
+
export const findSecretFiles = (workspace) => {
|
|
122
|
+
const list = (dir) => { try {
|
|
123
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
} };
|
|
128
|
+
const hit = (name) => SECRET_FILES.test(name) && !TEMPLATE.test(name);
|
|
129
|
+
const top = list(workspace);
|
|
130
|
+
const below = top.filter((d) => d.isDirectory() && !/^(node_modules|\.git|vendor|target|dist|build|\.venv|venv)$/i.test(d.name))
|
|
131
|
+
.flatMap((d) => list(path.join(workspace, d.name)).filter((f) => f.isFile() && hit(f.name)).map((f) => d.name + '\\' + f.name));
|
|
132
|
+
return [...top.filter((d) => d.isFile() && hit(d.name)).map((d) => d.name), ...below];
|
|
133
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const terminalDir: (dataPath: string) => string;
|
|
3
|
+
export declare const terminalExe: (dataPath: string) => string;
|
|
4
|
+
/** Our own profile id, so the profile is found again (and user edits to it are kept) on later launches. */
|
|
5
|
+
export declare const PWSH_PROFILE_GUID = "{5f3a6c1e-7d2b-4e8a-9c41-0b6d2a8f7e93}";
|
|
6
|
+
declare const TerminalSettings: z.ZodObject<{
|
|
7
|
+
profiles: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodObject<{
|
|
8
|
+
guid: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$loose>>, z.ZodObject<{
|
|
10
|
+
list: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
11
|
+
guid: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, z.core.$loose>>>;
|
|
13
|
+
}, z.core.$loose>]>>;
|
|
14
|
+
}, z.core.$loose>;
|
|
15
|
+
/**
|
|
16
|
+
* settings.json with the "PowerShell 7" profile shown when `visible`, else hidden (never deleted: a user's tweaks to it
|
|
17
|
+
* survive turning pwsh off and on). Pure: returns null when nothing changes.
|
|
18
|
+
*/
|
|
19
|
+
export declare const withPwshProfile: (settings: z.output<typeof TerminalSettings>, visible: boolean) => z.output<typeof TerminalSettings> | null;
|
|
20
|
+
/**
|
|
21
|
+
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
22
|
+
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
23
|
+
* shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
|
|
24
|
+
*/
|
|
25
|
+
export declare const syncPwshProfile: (dataPath: string, allowed: boolean) => void;
|
|
26
|
+
export declare const ensureTerminal: (dataPath: string, refresh: boolean, options?: {
|
|
27
|
+
pwsh?: boolean;
|
|
28
|
+
}) => Promise<{
|
|
29
|
+
exe: string | null;
|
|
30
|
+
message: string;
|
|
31
|
+
}>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Windows Terminal for the sandbox window. The host downloads the portable build once into persistent storage, so
|
|
2
|
+
// logon.cmd can open it the moment the sandbox logs in (no PowerShell window first).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { githubAsset, installArchive } from '../shared/download.js';
|
|
6
|
+
import { safeRemove } from '../shared/safe-remove.js';
|
|
7
|
+
import { assertTestWritable } from './test-guard.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { assertNoLinks, errMsg, fileExists, parseJsonc, writeJson } from '../core/util.js';
|
|
10
|
+
export const terminalDir = (dataPath) => path.join(dataPath, 'tools', 'terminal');
|
|
11
|
+
export const terminalExe = (dataPath) => path.join(terminalDir(dataPath), 'WindowsTerminal.exe');
|
|
12
|
+
/**
|
|
13
|
+
* Sandbox-friendly defaults; only written when there is no settings.json yet (first download), so edits made inside
|
|
14
|
+
* the sandbox stick, also across a refresh (which keeps the settings folder).
|
|
15
|
+
*/
|
|
16
|
+
const SETTINGS = {
|
|
17
|
+
$schema: 'https://aka.ms/terminal-profiles-schema',
|
|
18
|
+
theme: 'dark',
|
|
19
|
+
firstWindowPreference: 'defaultProfile',
|
|
20
|
+
confirmCloseAllTabs: false,
|
|
21
|
+
// New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
|
|
22
|
+
compatibility: { reloadEnvironmentVariables: true },
|
|
23
|
+
warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
|
|
24
|
+
profiles: { defaults: { font: { face: 'Cascadia Mono' }, startingDirectory: '%AGENT_WORKSPACE%', closeOnExit: 'graceful' } },
|
|
25
|
+
};
|
|
26
|
+
/** Our own profile id, so the profile is found again (and user edits to it are kept) on later launches. */
|
|
27
|
+
export const PWSH_PROFILE_GUID = '{5f3a6c1e-7d2b-4e8a-9c41-0b6d2a8f7e93}';
|
|
28
|
+
/** pwsh.exe as the sandbox sees it (toolchains/pwsh.ts installs it to tools\pwsh). */
|
|
29
|
+
const GUEST_PWSH = 'C:\\AgentData\\tools\\pwsh\\pwsh.exe';
|
|
30
|
+
const MAX_SETTINGS_BYTES = 1024 * 1024;
|
|
31
|
+
const TerminalSettings = z.looseObject({
|
|
32
|
+
profiles: z.union([z.array(z.looseObject({ guid: z.string().optional() })), z.looseObject({ list: z.array(z.looseObject({ guid: z.string().optional() })).optional() })]).optional(),
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* settings.json with the "PowerShell 7" profile shown when `visible`, else hidden (never deleted: a user's tweaks to it
|
|
36
|
+
* survive turning pwsh off and on). Pure: returns null when nothing changes.
|
|
37
|
+
*/
|
|
38
|
+
export const withPwshProfile = (settings, visible) => {
|
|
39
|
+
const profiles = Array.isArray(settings.profiles) ? { list: settings.profiles } : { ...settings.profiles };
|
|
40
|
+
const list = [...(profiles.list ?? [])];
|
|
41
|
+
const at = list.findIndex((p) => p.guid?.toLowerCase() === PWSH_PROFILE_GUID);
|
|
42
|
+
if (at === -1 && !visible)
|
|
43
|
+
return null;
|
|
44
|
+
if (at === -1)
|
|
45
|
+
list.push({ guid: PWSH_PROFILE_GUID, name: 'PowerShell 7', commandline: '"' + GUEST_PWSH + '" -NoLogo', icon: GUEST_PWSH, hidden: false });
|
|
46
|
+
else if ((list[at].hidden === true) === !visible)
|
|
47
|
+
return null;
|
|
48
|
+
else
|
|
49
|
+
list[at] = { ...list[at], hidden: !visible };
|
|
50
|
+
return { ...settings, profiles: { ...profiles, list } };
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
54
|
+
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
55
|
+
* shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
|
|
56
|
+
*/
|
|
57
|
+
export const syncPwshProfile = (dataPath, allowed) => {
|
|
58
|
+
const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
|
|
59
|
+
assertTestWritable(file);
|
|
60
|
+
assertNoLinks(dataPath, file);
|
|
61
|
+
if (!fileExists(file) || fs.statSync(file).size > MAX_SETTINGS_BYTES)
|
|
62
|
+
return;
|
|
63
|
+
const visible = allowed && fileExists(path.join(dataPath, 'tools', 'pwsh', 'pwsh.exe'));
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
const result = TerminalSettings.safeParse(parseJsonc(fs.readFileSync(file, 'utf8'), file));
|
|
67
|
+
if (!result.success)
|
|
68
|
+
return;
|
|
69
|
+
parsed = result.data;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const next = withPwshProfile(parsed, visible);
|
|
75
|
+
if (next)
|
|
76
|
+
writeJson(file, next);
|
|
77
|
+
};
|
|
78
|
+
export const ensureTerminal = async (dataPath, refresh, options = {}) => {
|
|
79
|
+
const result = await installTerminal(dataPath, refresh);
|
|
80
|
+
if (result.exe) {
|
|
81
|
+
// The profile is a nicety: a settings problem never costs the sandbox its terminal.
|
|
82
|
+
try {
|
|
83
|
+
syncPwshProfile(dataPath, options.pwsh ?? false);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return { ...result, message: result.message + ' (PowerShell 7 profile not updated: ' + errMsg(e) + ')' };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const installTerminal = async (dataPath, refresh) => {
|
|
92
|
+
const exe = terminalExe(dataPath);
|
|
93
|
+
const dir = terminalDir(dataPath);
|
|
94
|
+
assertTestWritable(dir);
|
|
95
|
+
const settings = path.join(dir, 'settings');
|
|
96
|
+
// installArchive replaces tools\terminal: the portable settings folder waits here meanwhile (and goes back also when
|
|
97
|
+
// the download fails). One still parked from an interrupted earlier run is the one to keep.
|
|
98
|
+
const parked = path.join(dataPath, 'tools', 'terminal-settings.parked');
|
|
99
|
+
assertNoLinks(dataPath, settings);
|
|
100
|
+
assertNoLinks(dataPath, parked);
|
|
101
|
+
if (fileExists(exe) && !refresh)
|
|
102
|
+
return { exe, message: 'cached' };
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(settings)) {
|
|
105
|
+
safeRemove(parked, path.dirname(parked));
|
|
106
|
+
fs.renameSync(settings, parked);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
110
|
+
const asset = await githubAsset('microsoft/terminal', new RegExp('^Microsoft\\.WindowsTerminal_[\\d.]+_' + arch + '\\.zip$'));
|
|
111
|
+
await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'WindowsTerminal.exe', fileName: asset.name });
|
|
112
|
+
fs.writeFileSync(path.join(dir, '.portable'), '');
|
|
113
|
+
return { exe, message: 'downloaded ' + asset.name };
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (fs.existsSync(parked)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
safeRemove(settings, path.dirname(settings));
|
|
119
|
+
fs.renameSync(parked, settings);
|
|
120
|
+
}
|
|
121
|
+
const settingsFile = path.join(settings, 'settings.json');
|
|
122
|
+
assertNoLinks(dataPath, settingsFile);
|
|
123
|
+
if (fileExists(path.join(dir, '.portable')) && !fs.existsSync(settingsFile))
|
|
124
|
+
writeJson(settingsFile, SETTINGS);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return { exe: null, message: 'download failed, the sandbox will use a plain PowerShell window (' + errMsg(e) + ')' };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { selfTestRunning } from '../shared/test-guard.js';
|
|
2
|
+
export { selfTestRunning };
|
|
3
|
+
/** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
|
|
4
|
+
export declare const refuseInSelfTest: (what: string) => void;
|
|
5
|
+
/** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
|
|
6
|
+
export declare const assertTestWritable: (target: string) => void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Host side of the self-test guard (see shared/test-guard.ts): outward actions refuse, with a message the user can read.
|
|
2
|
+
import { testWriteRefusal, selfTestRunning } from '../shared/test-guard.js';
|
|
3
|
+
import { fail } from '../core/util.js';
|
|
4
|
+
export { selfTestRunning };
|
|
5
|
+
/** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
|
|
6
|
+
export const refuseInSelfTest = (what) => {
|
|
7
|
+
if (selfTestRunning())
|
|
8
|
+
fail('The self-test does not ' + what + '.', 'It only works in temp folders, so it can never change this PC.');
|
|
9
|
+
};
|
|
10
|
+
/** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
|
|
11
|
+
export const assertTestWritable = (target) => {
|
|
12
|
+
const refusal = testWriteRefusal(target);
|
|
13
|
+
if (refusal)
|
|
14
|
+
fail(refusal);
|
|
15
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
|
|
2
|
+
export declare const hideSandboxWindow: (storageRoot: string, waitSeconds?: number) => Promise<boolean>;
|
|
3
|
+
/** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
|
|
4
|
+
export declare const showSandboxWindow: (storageRoot: string) => boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Headless mode: Windows Sandbox always opens its client window, so we hide it (ShowWindow SW_HIDE) as soon as it
|
|
2
|
+
// appears. The VM and the logged-in session keep running; `sbx-show` brings the window back.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeJsonAtomic } from '../shared/rundir.js';
|
|
5
|
+
import { refuseInSelfTest } from './test-guard.js';
|
|
6
|
+
import { isWin, runPowerShell, runPowerShellAsync, tryReadJson } from '../core/util.js';
|
|
7
|
+
const PRELUDE = `
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
Add-Type -Namespace AgentSandbox -Name Win -MemberDefinition @'
|
|
10
|
+
public delegate bool EnumProc(System.IntPtr hWnd, System.IntPtr lParam);
|
|
11
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, System.IntPtr lParam);
|
|
12
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint pid);
|
|
13
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool IsWindowVisible(System.IntPtr hWnd);
|
|
14
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool IsWindow(System.IntPtr hWnd);
|
|
15
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern System.IntPtr GetWindow(System.IntPtr hWnd, uint cmd);
|
|
16
|
+
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern int GetWindowText(System.IntPtr hWnd, System.Text.StringBuilder s, int n);
|
|
17
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool ShowWindow(System.IntPtr hWnd, int cmd);
|
|
18
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool SetForegroundWindow(System.IntPtr hWnd);
|
|
19
|
+
'@
|
|
20
|
+
function Get-SandboxWindows([bool]$visibleOnly) {
|
|
21
|
+
$pids = @(Get-Process | Where-Object { $_.ProcessName -like 'WindowsSandbox*' } | ForEach-Object { [uint32]$_.Id })
|
|
22
|
+
$found = New-Object System.Collections.ArrayList
|
|
23
|
+
if ($pids.Count -eq 0) { return $found }
|
|
24
|
+
$cb = [AgentSandbox.Win+EnumProc]{ param($h, $l)
|
|
25
|
+
$p = [uint32]0; [void][AgentSandbox.Win]::GetWindowThreadProcessId($h, [ref]$p)
|
|
26
|
+
if ($pids -contains $p -and [AgentSandbox.Win]::GetWindow($h, 4) -eq [IntPtr]::Zero) {
|
|
27
|
+
$sb = New-Object System.Text.StringBuilder 256; [void][AgentSandbox.Win]::GetWindowText($h, $sb, 256)
|
|
28
|
+
$title = $sb.ToString()
|
|
29
|
+
if ($title -like '*Sandbox*' -and (-not $visibleOnly -or [AgentSandbox.Win]::IsWindowVisible($h))) { [void]$found.Add([int64]$h) }
|
|
30
|
+
}
|
|
31
|
+
return $true }
|
|
32
|
+
[void][AgentSandbox.Win]::EnumWindows($cb, [IntPtr]::Zero)
|
|
33
|
+
return $found
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const stateFile = (storageRoot) => path.join(storageRoot, 'run', 'hidden-windows.json');
|
|
37
|
+
/** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
|
|
38
|
+
export const hideSandboxWindow = async (storageRoot, waitSeconds = 180) => {
|
|
39
|
+
refuseInSelfTest('hide windows');
|
|
40
|
+
if (!isWin)
|
|
41
|
+
return false;
|
|
42
|
+
const script = PRELUDE + `
|
|
43
|
+
$hidden = New-Object System.Collections.ArrayList
|
|
44
|
+
$deadline = (Get-Date).AddSeconds(${waitSeconds}); $settle = $null
|
|
45
|
+
while ((Get-Date) -lt $deadline) {
|
|
46
|
+
foreach ($h in (Get-SandboxWindows $true)) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 0); if (-not $hidden.Contains($h)) { [void]$hidden.Add($h) } }
|
|
47
|
+
if ($hidden.Count -gt 0 -and $null -eq $settle) { $settle = (Get-Date).AddSeconds(15) }
|
|
48
|
+
if ($null -ne $settle -and (Get-Date) -gt $settle) { break }
|
|
49
|
+
Start-Sleep -Milliseconds 200
|
|
50
|
+
}
|
|
51
|
+
ConvertTo-Json -Compress @($hidden)`;
|
|
52
|
+
const stdout = await runPowerShellAsync(script);
|
|
53
|
+
const handles = JSON.parse(stdout.trim() || '[]').filter((h) => Number.isSafeInteger(h));
|
|
54
|
+
writeJsonAtomic(stateFile(storageRoot), { handles });
|
|
55
|
+
return handles.length > 0;
|
|
56
|
+
};
|
|
57
|
+
/** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
|
|
58
|
+
export const showSandboxWindow = (storageRoot) => {
|
|
59
|
+
refuseInSelfTest('show windows');
|
|
60
|
+
if (!isWin)
|
|
61
|
+
return false;
|
|
62
|
+
// Only integers go into the script, whatever the file contains.
|
|
63
|
+
const recorded = (tryReadJson(stateFile(storageRoot))?.handles ?? []).filter((h) => Number.isSafeInteger(h));
|
|
64
|
+
const script = PRELUDE + `
|
|
65
|
+
$handles = @(${recorded.join(',')}) | Where-Object { [AgentSandbox.Win]::IsWindow([IntPtr]$_) }
|
|
66
|
+
if (-not $handles) { $handles = Get-SandboxWindows $false }
|
|
67
|
+
foreach ($h in $handles) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 5); [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 9); [void][AgentSandbox.Win]::SetForegroundWindow([IntPtr]$h) }
|
|
68
|
+
@($handles).Count`;
|
|
69
|
+
const { stdout } = runPowerShell(script, true);
|
|
70
|
+
writeJsonAtomic(stateFile(storageRoot), { handles: [] });
|
|
71
|
+
return Number(stdout.trim()) > 0;
|
|
72
|
+
};
|