windows-development-sandbox 1.10.0 → 1.11.4
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/README.md +64 -17
- package/config.example.json +5 -1
- package/dist/app/features/accounts/accounts.def.d.ts +122 -0
- package/dist/app/features/accounts/accounts.def.js +243 -0
- package/dist/app/features/accounts/accounts.run.d.ts +51 -0
- package/dist/app/features/accounts/accounts.run.js +125 -0
- package/dist/app/features/accounts/index.d.ts +2 -0
- package/dist/app/features/accounts/index.js +3 -0
- package/dist/app/features/agents/agent-entry.d.ts +17 -0
- package/dist/app/features/agents/agent-entry.js +50 -0
- package/dist/app/features/agents/agents.def.d.ts +65 -0
- package/dist/app/features/agents/agents.def.js +142 -0
- package/dist/app/features/agents/agents.run.d.ts +31 -0
- package/dist/app/features/agents/agents.run.js +189 -0
- package/dist/app/features/agents/index.d.ts +3 -0
- package/dist/app/features/agents/index.js +4 -0
- package/dist/app/features/apps/apps-rows.d.ts +31 -0
- package/dist/app/features/apps/apps-rows.js +73 -0
- package/dist/app/features/apps/apps.def.d.ts +181 -0
- package/dist/app/features/apps/apps.def.js +264 -0
- package/dist/app/features/apps/apps.run.d.ts +58 -0
- package/dist/app/features/apps/apps.run.js +76 -0
- package/dist/app/features/apps/index.d.ts +3 -0
- package/dist/app/features/apps/index.js +4 -0
- package/dist/app/features/data/data.def.d.ts +154 -0
- package/dist/app/features/data/data.def.js +194 -0
- package/dist/app/features/data/data.run.d.ts +46 -0
- package/dist/app/features/data/data.run.js +141 -0
- package/dist/app/features/data/index.d.ts +2 -0
- package/dist/app/features/data/index.js +3 -0
- package/dist/app/features/defs.d.ts +959 -4
- package/dist/app/features/defs.js +151 -10
- package/dist/app/features/engine/engine.def.d.ts +228 -0
- package/dist/app/features/engine/engine.def.js +194 -0
- package/dist/app/features/engine/engine.run.d.ts +72 -0
- package/dist/app/features/engine/engine.run.js +77 -0
- package/dist/app/features/engine/index.d.ts +2 -0
- package/dist/app/features/engine/index.js +4 -0
- package/dist/app/features/files/files.def.d.ts +51 -0
- package/dist/app/features/files/files.def.js +38 -0
- package/dist/app/features/files/files.run.d.ts +20 -0
- package/dist/app/features/files/files.run.js +59 -0
- package/dist/app/features/files/index.d.ts +2 -0
- package/dist/app/features/files/index.js +3 -0
- package/dist/app/features/github/github.def.d.ts +80 -0
- package/dist/app/features/github/github.def.js +142 -0
- package/dist/app/features/github/github.run.d.ts +36 -0
- package/dist/app/features/github/github.run.js +59 -0
- package/dist/app/features/github/index.d.ts +2 -0
- package/dist/app/features/github/index.js +3 -0
- package/dist/app/features/health/health.def.d.ts +275 -0
- package/dist/app/features/health/health.def.js +297 -0
- package/dist/app/features/health/health.run.d.ts +125 -0
- package/dist/app/features/health/health.run.js +204 -0
- package/dist/app/features/health/index.d.ts +2 -0
- package/dist/app/features/health/index.js +3 -0
- package/dist/app/features/logs/index.d.ts +2 -0
- package/dist/app/features/logs/index.js +3 -0
- package/dist/app/features/logs/logs.def.d.ts +78 -0
- package/dist/app/features/logs/logs.def.js +115 -0
- package/dist/app/features/logs/logs.run.d.ts +20 -0
- package/dist/app/features/logs/logs.run.js +42 -0
- package/dist/app/features/projects/index.d.ts +2 -0
- package/dist/app/features/projects/index.js +3 -0
- package/dist/app/features/projects/projects.def.d.ts +101 -0
- package/dist/app/features/projects/projects.def.js +208 -0
- package/dist/app/features/projects/projects.run.d.ts +56 -0
- package/dist/app/features/projects/projects.run.js +155 -0
- package/dist/app/features/sandbox/sandbox.def.js +11 -5
- package/dist/app/features/sessions/sessions.def.js +3 -0
- package/dist/app/features/settings/categories.def.js +3 -2
- package/dist/{cli/menu/settings/share.d.ts → app/features/settings/settings-share.d.ts} +1 -1
- package/dist/{cli/menu/settings/share.js → app/features/settings/settings-share.js} +3 -2
- package/dist/app/features/settings/settings.def.d.ts +22 -5
- package/dist/app/features/settings/settings.def.js +81 -6
- package/dist/app/features/settings/settings.run.d.ts +17 -0
- package/dist/app/features/settings/settings.run.js +89 -4
- package/dist/app/features/storage/index.d.ts +2 -0
- package/dist/app/features/storage/index.js +3 -0
- package/dist/app/features/storage/storage.def.d.ts +133 -0
- package/dist/app/features/storage/storage.def.js +220 -0
- package/dist/app/features/storage/storage.run.d.ts +62 -0
- package/dist/app/features/storage/storage.run.js +370 -0
- package/dist/app/features/updates/index.d.ts +3 -0
- package/dist/app/features/updates/index.js +4 -0
- package/dist/app/features/updates/update-lines.d.ts +13 -0
- package/dist/app/features/updates/update-lines.js +61 -0
- package/dist/app/features/updates/updates.def.d.ts +89 -0
- package/dist/app/features/updates/updates.def.js +224 -0
- package/dist/app/features/updates/updates.run.d.ts +48 -0
- package/dist/app/features/updates/updates.run.js +154 -0
- package/dist/app/features/vpn/index.d.ts +2 -0
- package/dist/app/features/vpn/index.js +3 -0
- package/dist/app/features/vpn/vpn.def.d.ts +112 -0
- package/dist/app/features/vpn/vpn.def.js +174 -0
- package/dist/app/features/vpn/vpn.run.d.ts +37 -0
- package/dist/app/features/vpn/vpn.run.js +54 -0
- package/dist/app/features/web/index.d.ts +2 -0
- package/dist/app/features/web/index.js +3 -0
- package/dist/app/features/web/web.def.d.ts +33 -0
- package/dist/app/features/web/web.def.js +90 -0
- package/dist/app/features/web/web.run.d.ts +24 -0
- package/dist/app/features/web/web.run.js +93 -0
- package/dist/app/kernel/define.d.ts +10 -1
- package/dist/app/kernel/io.d.ts +34 -0
- package/dist/app/kernel/io.js +9 -0
- package/dist/app/kernel/rpc.d.ts +17 -0
- package/dist/app/kernel/text.d.ts +2 -0
- package/dist/app/kernel/text.js +7 -0
- package/dist/app/kernel/view.d.ts +24 -1
- package/dist/app/kernel/view.js +1 -1
- package/dist/app/registry.js +74 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/commands/context.d.ts +7 -0
- package/dist/cli/commands/context.js +29 -2
- package/dist/cli/commands/engine.js +5 -58
- package/dist/cli/commands/github.d.ts +2 -0
- package/dist/cli/commands/github.js +11 -0
- package/dist/cli/commands/index.d.ts +3 -5
- package/dist/cli/commands/index.js +3 -5
- package/dist/cli/commands/launch/launch.js +21 -4
- package/dist/cli/commands/launch/running.js +4 -1
- package/dist/cli/commands/maintenance/config.js +1 -6
- package/dist/cli/commands/maintenance/index.d.ts +0 -1
- package/dist/cli/commands/maintenance/index.js +1 -2
- package/dist/cli/commands/maintenance/logs.js +27 -32
- package/dist/cli/commands/maintenance/migrate.d.ts +0 -8
- package/dist/cli/commands/maintenance/migrate.js +2 -44
- package/dist/cli/commands/maintenance/update.d.ts +0 -19
- package/dist/cli/commands/maintenance/update.js +1 -99
- package/dist/cli/commands/projects/index.d.ts +0 -1
- package/dist/cli/commands/projects/index.js +0 -1
- package/dist/cli/commands/registry.d.ts +1 -1
- package/dist/cli/commands/registry.js +29 -11
- package/dist/cli/commands/repair/checks/data.js +1 -1
- package/dist/cli/commands/repair/checks/hcs.js +28 -1
- package/dist/cli/commands/repair/checks/sandbox.js +1 -1
- package/dist/cli/commands/repair/health-run.d.ts +30 -0
- package/dist/cli/commands/repair/health-run.js +32 -0
- package/dist/cli/commands/repair/printer.d.ts +2 -2
- package/dist/cli/commands/repair/printer.js +2 -11
- package/dist/cli/commands/repair/run.d.ts +1 -21
- package/dist/cli/commands/repair/run.js +13 -70
- package/dist/cli/commands/storage/index.d.ts +0 -1
- package/dist/cli/commands/storage/index.js +0 -1
- package/dist/cli/commands/storage/offer.js +6 -2
- package/dist/cli/commands/vpn.d.ts +1 -3
- package/dist/cli/commands/vpn.js +8 -89
- package/dist/cli/commands/web.d.ts +2 -1
- package/dist/cli/commands/web.js +14 -78
- package/dist/cli/index.js +3 -2
- package/dist/cli/manifest.d.ts +16 -153
- package/dist/cli/manifest.js +20 -50
- package/dist/cli/menu/command-form.js +1 -2
- package/dist/cli/menu/io-menu.d.ts +2 -2
- package/dist/cli/menu/io-menu.js +6 -4
- package/dist/cli/menu/menu.js +1 -1
- package/dist/cli/menu/pages/accounts-page.d.ts +5 -17
- package/dist/cli/menu/pages/accounts-page.js +28 -170
- package/dist/cli/menu/pages/agents-page.d.ts +17 -18
- package/dist/cli/menu/pages/agents-page.js +67 -124
- package/dist/cli/menu/pages/apps-page.d.ts +15 -8
- package/dist/cli/menu/pages/apps-page.js +121 -29
- package/dist/cli/menu/pages/list-page.js +10 -1
- package/dist/cli/menu/pages/maintenance-page.d.ts +51 -52
- package/dist/cli/menu/pages/maintenance-page.js +246 -427
- package/dist/cli/menu/pages/sandbox-page.d.ts +3 -5
- package/dist/cli/menu/pages/sandbox-page.js +34 -112
- package/dist/cli/menu/pages/settings-page.d.ts +21 -3
- package/dist/cli/menu/pages/settings-page.js +89 -17
- package/dist/cli/menu/render-view.js +2 -1
- package/dist/cli/menu/settings/index.d.ts +0 -1
- package/dist/cli/menu/settings/index.js +3 -3
- package/dist/cli/menu/views.js +28 -0
- package/dist/cli/operations/handler.js +7 -3
- package/dist/cli/operations/input.d.ts +1 -1
- package/dist/cli/operations/input.js +29 -5
- package/dist/cli/operations/io-cli.js +9 -1
- package/dist/cli/operations/spec.js +8 -2
- package/dist/cli/output/render-view.js +2 -0
- package/dist/cli/{commands/data/collect.js → services/data-collect.js} +2 -2
- package/dist/cli/{commands/data/remove.js → services/data-remove.js} +2 -2
- package/dist/cli/services/data-service.d.ts +1 -1
- package/dist/cli/services/data-service.js +2 -2
- package/dist/cli/services/databases-service.d.ts +5 -0
- package/dist/cli/services/databases-service.js +15 -0
- package/dist/cli/services/health-service.d.ts +8 -4
- package/dist/cli/services/health-service.js +10 -8
- package/dist/cli/services/index.d.ts +4 -1
- package/dist/cli/services/index.js +4 -1
- package/dist/cli/services/logs-service.d.ts +7 -0
- package/dist/cli/services/logs-service.js +13 -0
- package/dist/cli/services/storage-service.d.ts +19 -0
- package/dist/cli/services/storage-service.js +71 -0
- package/dist/cli/services/vpn-service.d.ts +6 -4
- package/dist/cli/services/vpn-service.js +5 -11
- package/dist/cli/services/web-service.d.ts +4 -3
- package/dist/cli/services/web-service.js +12 -32
- package/dist/core/config/defaults.js +2 -1
- package/dist/core/schema/config.d.ts +5 -0
- package/dist/core/schema/config.js +2 -0
- package/dist/core/schema/github.d.ts +4 -0
- package/dist/core/schema/github.js +7 -0
- package/dist/core/schema/index.d.ts +1 -0
- package/dist/core/schema/index.js +1 -0
- package/dist/core/schema/profile.d.ts +1 -0
- package/dist/core/schema/profile.js +3 -0
- package/dist/core/schema/sandbox.js +3 -3
- package/dist/core/schema/toolchains.js +2 -2
- package/dist/core/types.d.ts +6 -3
- package/dist/guest/boot/boot.js +8 -0
- package/dist/guest/daemon/daemon.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +3 -2
- package/dist/guest/daemon/sessions.js +38 -7
- package/dist/guest/governor/core.d.ts +44 -0
- package/dist/guest/governor/core.js +124 -0
- package/dist/guest/governor/decide.d.ts +46 -18
- package/dist/guest/governor/decide.js +136 -82
- package/dist/guest/governor/governor.d.ts +2 -38
- package/dist/guest/governor/governor.js +33 -113
- package/dist/guest/governor/index.d.ts +1 -0
- package/dist/guest/governor/index.js +1 -0
- package/dist/guest/network/firewall.d.ts +2 -0
- package/dist/guest/network/firewall.js +3 -3
- package/dist/guest/sessions/viewer.js +5 -1
- package/dist/guest/setup/github-token.d.ts +7 -0
- package/dist/guest/setup/github-token.js +65 -0
- package/dist/guest/toolchains/languages/node.js +11 -1
- package/dist/guest/toolchains/native/rust-extras.js +2 -2
- package/dist/guest/toolchains/tools/cli.d.ts +8 -0
- package/dist/guest/toolchains/tools/cli.js +35 -2
- package/dist/guest/vpn/openvpn.d.ts +5 -0
- package/dist/guest/vpn/openvpn.js +14 -1
- package/dist/guest/vpn/vpn.js +55 -16
- package/dist/guest/vpn/wireguard.js +2 -0
- package/dist/host/channel/attach.d.ts +1 -0
- package/dist/host/channel/attach.js +1 -0
- package/dist/host/channel/channel.d.ts +1 -1
- package/dist/host/channel/channel.js +2 -2
- package/dist/host/engines/hcs/check.d.ts +1 -0
- package/dist/host/engines/hcs/check.js +1 -1
- package/dist/host/engines/hcs/engine.d.ts +1 -1
- package/dist/host/engines/hcs/engine.js +6 -4
- package/dist/host/engines/hcs/helper.d.ts +11 -0
- package/dist/host/engines/hcs/helper.js +31 -2
- package/dist/host/engines/hcs/leftovers.d.ts +42 -0
- package/dist/host/engines/hcs/leftovers.js +101 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/github/deliver.d.ts +13 -0
- package/dist/host/github/deliver.js +51 -0
- package/dist/host/github/index.d.ts +2 -0
- package/dist/host/github/index.js +3 -0
- package/dist/host/github/token.d.ts +21 -0
- package/dist/host/github/token.js +74 -0
- package/dist/host/launch/generate.js +11 -5
- package/dist/host/launch/index.d.ts +1 -1
- package/dist/host/launch/index.js +1 -1
- package/dist/host/launch/session-file.js +1 -2
- package/dist/host/launch/wsb-file.d.ts +11 -7
- package/dist/host/launch/wsb-file.js +30 -12
- package/dist/host/life/host-memory.d.ts +18 -0
- package/dist/host/life/host-memory.js +66 -0
- package/dist/host/life/watchdog.d.ts +9 -0
- package/dist/host/life/watchdog.js +17 -1
- package/dist/host/sandbox/tasklist.d.ts +6 -0
- package/dist/host/sandbox/tasklist.js +37 -0
- package/dist/host/secrets/launch-secrets.d.ts +2 -1
- package/dist/host/secrets/launch-secrets.js +4 -2
- package/dist/host/vpn/status.js +13 -3
- package/dist/host/web/backend.d.ts +1 -3
- package/dist/host/web/index.d.ts +2 -1
- package/dist/host/web/index.js +2 -1
- package/dist/host/web/live.d.ts +23 -0
- package/dist/host/web/live.js +107 -0
- package/dist/host/web/rpc.d.ts +6 -1
- package/dist/host/web/rpc.js +23 -31
- package/dist/host/web/runner.d.ts +4 -0
- package/dist/host/web/runner.js +12 -1
- package/dist/host/web/server.d.ts +30 -2
- package/dist/host/web/server.js +87 -29
- package/dist/host/web/state.d.ts +1 -0
- package/dist/host/web/state.js +2 -0
- package/dist/runtime/agent.js +30 -6
- package/dist/runtime/daemon.js +337 -177
- package/dist/runtime/db.js +30 -6
- package/dist/runtime/main.js +389 -77
- package/dist/runtime/netproxy.js +30 -6
- package/dist/runtime/view.js +495 -146
- package/dist/selftest/app.test.js +632 -12
- package/dist/selftest/channel.test.js +18 -1
- package/dist/selftest/cli.test.js +1 -1
- package/dist/selftest/config.test.js +1 -1
- package/dist/selftest/contained.test.js +1 -1
- package/dist/selftest/data.test.js +1 -2
- package/dist/selftest/engine.test.js +3 -1
- package/dist/selftest/github.test.d.ts +2 -0
- package/dist/selftest/github.test.js +243 -0
- package/dist/selftest/hcs.test.js +105 -8
- package/dist/selftest/health.test.js +71 -0
- package/dist/selftest/heavy-load.test.js +132 -72
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/layout.test.js +8 -0
- package/dist/selftest/live-terminal.test.js +74 -0
- package/dist/selftest/menu-screens.js +171 -56
- package/dist/selftest/menu.test.js +54 -37
- package/dist/selftest/migrate.test.js +14 -12
- package/dist/selftest/powershell-variables.test.d.ts +2 -0
- package/dist/selftest/powershell-variables.test.js +61 -0
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +4 -0
- package/dist/selftest/support/web-preview.d.ts +1 -0
- package/dist/selftest/support/web-preview.js +856 -0
- package/dist/selftest/update-service.test.js +15 -8
- package/dist/selftest/vpn.test.js +86 -2
- package/dist/selftest/web.test.js +58 -11
- package/dist/shared/channel/run-dir.d.ts +2 -0
- package/dist/shared/channel/run-dir.js +3 -0
- package/dist/shared/channel/schema.d.ts +7 -0
- package/dist/shared/channel/schema.js +9 -1
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +3 -0
- package/dist/shared/github/index.d.ts +1 -0
- package/dist/shared/github/index.js +2 -0
- package/dist/shared/github/token.d.ts +29 -0
- package/dist/shared/github/token.js +89 -0
- package/dist/shared/memory/policy.d.ts +33 -12
- package/dist/shared/memory/policy.js +50 -19
- package/dist/shared/pty/ctrl-c.d.ts +18 -0
- package/dist/shared/pty/ctrl-c.js +36 -0
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +2 -0
- package/dist/shared/pty/terminal.js +21 -2
- package/dist/shared/vpn/kill-switch.d.ts +13 -2
- package/dist/shared/vpn/kill-switch.js +88 -8
- package/dist/shared/web/protocol.d.ts +0 -4
- package/dist/sync/watcher.d.ts +2 -0
- package/dist/sync/watcher.js +8 -3
- package/dist/web/index.html +2 -2
- package/dist/web/{main-T5DMCHYV.css → main-MU5XWZMS.css} +1 -1
- package/dist/web/main-SLQNJRXD.js +219 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/MemoryStatus.cs +58 -2
- package/native/csharp/HostTools/Program.cs +7 -1
- package/package.json +10 -3
- package/sandbox/csharp/MemGov.cs +240 -38
- package/sandbox/csharp/PtyHost.cs +104 -15
- package/sandbox/csharp/SandboxTools/CpuCap.cs +50 -1
- package/sandbox/csharp/SandboxTools/Program.cs +1 -1
- package/sandbox/scripts/repair-wmi-services.ps1 +26 -0
- package/schemas/config.schema.json +19 -5
- package/schemas/sandbox-config.schema.json +7 -3
- package/dist/cli/commands/accounts.d.ts +0 -2
- package/dist/cli/commands/accounts.js +0 -96
- package/dist/cli/commands/data/clean.d.ts +0 -3
- package/dist/cli/commands/data/clean.js +0 -60
- package/dist/cli/commands/data/index.d.ts +0 -4
- package/dist/cli/commands/data/index.js +0 -62
- package/dist/cli/commands/maintenance/clean.d.ts +0 -2
- package/dist/cli/commands/maintenance/clean.js +0 -24
- package/dist/cli/commands/maintenance/config-share.d.ts +0 -12
- package/dist/cli/commands/maintenance/config-share.js +0 -80
- package/dist/cli/commands/projects/projects.d.ts +0 -2
- package/dist/cli/commands/projects/projects.js +0 -162
- package/dist/cli/commands/storage/contained.d.ts +0 -3
- package/dist/cli/commands/storage/contained.js +0 -179
- package/dist/cli/commands/storage/storage.d.ts +0 -8
- package/dist/cli/commands/storage/storage.js +0 -179
- package/dist/cli/menu/add-agent.d.ts +0 -47
- package/dist/cli/menu/add-agent.js +0 -168
- package/dist/cli/menu/apps.d.ts +0 -69
- package/dist/cli/menu/apps.js +0 -214
- package/dist/cli/menu/data-folder.d.ts +0 -18
- package/dist/cli/menu/data-folder.js +0 -99
- package/dist/cli/menu/share-settings.d.ts +0 -21
- package/dist/cli/menu/share-settings.js +0 -112
- package/dist/cli/menu/vpn-login.d.ts +0 -16
- package/dist/cli/menu/vpn-login.js +0 -74
- package/dist/web/main-NRRS5O6Z.js +0 -157
- /package/dist/cli/{commands/data/collect.d.ts → services/data-collect.d.ts} +0 -0
- /package/dist/cli/{commands/data/remove.d.ts → services/data-remove.d.ts} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { safeId } from '../shared/channel/index.js';
|
|
4
4
|
import { GuestRequestSchema, SandboxStatusSchema, SessionStatusSchema } from '../shared/channel/index.js';
|
|
5
5
|
import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../cli/commands/launch/index.js';
|
|
6
|
-
import { encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, MAX_FRAME_BYTES, MOUSE_OFF, OutputFilter, } from '../shared/pty/index.js';
|
|
6
|
+
import { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard, encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, MAX_FRAME_BYTES, MOUSE_OFF, OutputFilter, } from '../shared/pty/index.js';
|
|
7
7
|
import { bootSilent, FIRST_STATUS_MS } from '../host/channel/index.js';
|
|
8
8
|
export const channelSuite = {
|
|
9
9
|
name: 'channel',
|
|
@@ -132,6 +132,23 @@ export const channelSuite = {
|
|
|
132
132
|
hello.payload[4] === 2 &&
|
|
133
133
|
hello.payload.subarray(5).toString() === 'tok', 'a frame of exactly the maximum size is accepted; a hello frame carries size, flags and token');
|
|
134
134
|
t.equal(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])), Buffer.from([0x61, 0x7f, 0x08, 0x1b]), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
|
|
135
|
+
t.section('Ctrl+C meant as "copy" does not reach the agent');
|
|
136
|
+
const ctrlC = Buffer.from([3]);
|
|
137
|
+
const g = new CtrlCGuard();
|
|
138
|
+
const first = g.push(ctrlC, 1000);
|
|
139
|
+
const second = g.push(ctrlC, 1000 + CTRL_C_CONFIRM_MS - 1);
|
|
140
|
+
const third = g.push(ctrlC, 5000);
|
|
141
|
+
const late = g.push(ctrlC, 5000 + CTRL_C_CONFIRM_MS + 1);
|
|
142
|
+
t.check(first.held &&
|
|
143
|
+
first.send.length === 0 &&
|
|
144
|
+
!second.held &&
|
|
145
|
+
second.send.equals(ctrlC) &&
|
|
146
|
+
third.held &&
|
|
147
|
+
late.held &&
|
|
148
|
+
!g.push(Buffer.from('a\x03'), 6000).held &&
|
|
149
|
+
!g.push(Buffer.from('x'), 6000).held, 'a lone Ctrl+C is held back, a second one soon after is sent, a late second one is held again, other input passes');
|
|
150
|
+
g.watchOutput('\x1b]0;claude: working\x07more \x1b]2;\u0001evil\x07');
|
|
151
|
+
t.check(g.hintTitle().includes(CTRL_C_HINT) && g.restoreTitle() === '\x1b]0;evil\x07', "the hint shows in the title, and the program's last title comes back without control characters");
|
|
135
152
|
t.section('typed input keeps escape sequences whole');
|
|
136
153
|
const joined = (...chunks) => {
|
|
137
154
|
const j = new InputJoiner();
|
|
@@ -8,7 +8,7 @@ import { COMMANDS } from '../cli/manifest.js';
|
|
|
8
8
|
import { commandLine, readHistory, recordHistory } from '../cli/history.js';
|
|
9
9
|
import { splitArgs } from '../cli/args.js';
|
|
10
10
|
import { optionProblem } from '../cli/menu/index.js';
|
|
11
|
-
import { cleanTargetOf } from '../cli/
|
|
11
|
+
import { cleanTargetOf } from '../cli/services/index.js';
|
|
12
12
|
import { ago, colorEnabled, shorten } from '../cli/output/index.js';
|
|
13
13
|
import { versionText } from '../cli/version.js';
|
|
14
14
|
import { hoistConfig, withoutConfig } from '../cli/argv.js';
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { DEFAULTS, loadConfig } from '../core/config/index.js';
|
|
6
|
-
import { diffText, exportSettings, planImport } from '../
|
|
6
|
+
import { diffText, exportSettings, planImport } from '../app/features/settings/settings-share.js';
|
|
7
7
|
import { readJsonc } from '../core/util/index.js';
|
|
8
8
|
export const configSuite = {
|
|
9
9
|
name: 'config',
|
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import { assertNoGuestOverlap } from '../core/config/index.js';
|
|
10
10
|
import { isWin } from '../core/util/index.js';
|
|
11
11
|
import { CONTAINED_CHECKS } from '../cli/commands/repair/checks/contained.js';
|
|
12
|
-
import { storageBusy } from '../cli/
|
|
12
|
+
import { storageBusy } from '../cli/services/index.js';
|
|
13
13
|
import { generateSandbox, GUEST } from '../host/launch/index.js';
|
|
14
14
|
import { currentUserSid } from '../host/sharing/index.js';
|
|
15
15
|
import { addHolder, CONTAINED_FOLDERS, containedAvailability, ContainedStorage, containedMappings, containedPaths, diagnose, hostDataFolder, markMounted, mountedSets, planTransfer, providerFor, pruneHolders, removeHolder, runTransfer, ScriptDiskPort, setNameOf, setStateOf, staleMountNote, StateFile, } from '../host/storage/index.js';
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// launch folders, logs and downloads are pruned.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { resolveCleanTargets } from '../cli/commands/data/clean.js';
|
|
6
|
-
import { removeTree } from '../cli/commands/data/remove.js';
|
|
7
5
|
import { findLinks } from '../cli/commands/repair/finders.js';
|
|
6
|
+
import { removeTree, resolveCleanTargets } from '../cli/services/index.js';
|
|
8
7
|
import { humanBytes, sizeOf } from '../core/disk.js';
|
|
9
8
|
import { pruneStorage } from '../host/storage/index.js';
|
|
10
9
|
export const dataSuite = {
|
|
@@ -66,7 +66,9 @@ export const engineSuite = {
|
|
|
66
66
|
t.equal(fs.readFileSync(gen.wsbPath, 'utf8'), legacyWsb(f.baseConfig, gen.memoryMb, gen.mappings), 'the launch writes the .wsb the old writer would have (same settings, memory and mappings)');
|
|
67
67
|
t.equal(path.basename(gen.wsbPath), 'agent.wsb', 'the engine file is still agent.wsb in the launch folder');
|
|
68
68
|
t.equal(gen.enginePlan.entry, gen.wsbPath, 'the engine plan starts from that file');
|
|
69
|
-
|
|
69
|
+
// home/.gitconfig is written only when this machine's ~/.gitconfig has a name or email (a GitHub runner has none).
|
|
70
|
+
const sameOnEveryMachine = (f) => f !== 'home/.gitconfig';
|
|
71
|
+
t.equal(filesUnder(gen.launchPath).filter(sameOnEveryMachine), LAUNCH_FILES_GOLDEN.filter(sameOnEveryMachine), 'the launch folder has exactly the files it had before');
|
|
70
72
|
t.equal(gen.mappings.find((m) => m.guest === 'C:\\AgentData')?.host, f.baseConfig.storage.root + '\\data\\testdata', 'the data folder is still mapped at C:\\AgentData (through the folder provider)');
|
|
71
73
|
t.section('capabilities');
|
|
72
74
|
const caps = (over, cli) => new WindowsSandboxEngine({ ...DEFAULTS.sandbox, ...over }, () => cli).capabilities();
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// The GitHub token for sandboxes: which tokens are accepted, the sealed store, when a launch hands it over (only in the
|
|
2
|
+
// one-shot secrets file), the shared-core operations, the project-file rules, and the gh commands the sandbox runs.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { deleteGithubTokenFlow, githubPage, githubTokenRowOf, saveGithubTokenFlow, } from '../app/features/github/index.js';
|
|
6
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
7
|
+
import { runDefinition } from '../app/registry.js';
|
|
8
|
+
import { pageLines } from '../cli/output/index.js';
|
|
9
|
+
import { DEFAULTS } from '../core/config/index.js';
|
|
10
|
+
import { applyOverrides, normalizeOverrides } from '../core/local-config/index.js';
|
|
11
|
+
import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
|
|
12
|
+
import { checkGithubToken, GithubToken, githubTokenAllowed, prepareGithubToken } from '../host/github/index.js';
|
|
13
|
+
import { generateSandbox } from '../host/launch/index.js';
|
|
14
|
+
import { insideSandbox } from '../host/sandbox/platform.js';
|
|
15
|
+
import { clearSecrets, SecretStore } from '../host/secrets/index.js';
|
|
16
|
+
import { RunDir } from '../shared/channel/index.js';
|
|
17
|
+
import { SecretsFileSchema } from '../shared/channel/secrets.js';
|
|
18
|
+
import { ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenProblem } from '../shared/github/index.js';
|
|
19
|
+
const FINE = 'github_pat_' + 'PlantedFine0123456789'.repeat(3);
|
|
20
|
+
const CLASSIC = 'ghp_' + 'PlantedClassic0123456789ab';
|
|
21
|
+
const OAUTH = 'gho_' + 'PlantedOauth0123456789abcd';
|
|
22
|
+
/** Every file under a folder, as text. */
|
|
23
|
+
function allText(dir) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const walk = (d) => {
|
|
26
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
27
|
+
const p = path.join(d, e.name);
|
|
28
|
+
if (e.isDirectory())
|
|
29
|
+
walk(p);
|
|
30
|
+
else
|
|
31
|
+
out.push(fs.readFileSync(p, 'latin1'));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(dir);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
const message = async (work) => {
|
|
38
|
+
try {
|
|
39
|
+
await work();
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
const err = e;
|
|
44
|
+
return err.message + ' ' + (err.fix ?? '');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export const githubSuite = {
|
|
48
|
+
name: 'github',
|
|
49
|
+
title: 'GitHub token: accepted tokens, sealed store, delivery only in the one-shot file, operations, gh on stdin',
|
|
50
|
+
run: async (t, f) => {
|
|
51
|
+
t.section('which tokens are accepted');
|
|
52
|
+
t.check(checkGithubToken(FINE) === 'fine-grained', 'a fine-grained token (github_pat_) is accepted');
|
|
53
|
+
t.check(checkGithubToken(CLASSIC) === 'classic', 'a classic token (ghp_) is accepted');
|
|
54
|
+
for (const [bad, why, expect] of [
|
|
55
|
+
[OAUTH, 'an OAuth token from gh auth token', 'OAuth'],
|
|
56
|
+
['not-a-token-' + 'x'.repeat(20), 'text that is not a token', 'github_pat_'],
|
|
57
|
+
['github_pat_ ' + 'x'.repeat(30), 'a token with a space', 'spaces'],
|
|
58
|
+
['github_pat_\n' + 'x'.repeat(30), 'a token with a line break', 'line breaks'],
|
|
59
|
+
['ghp_short', 'a token too short', 'length'],
|
|
60
|
+
['github_pat_' + 'x'.repeat(300), 'a token too long', 'length'],
|
|
61
|
+
]) {
|
|
62
|
+
const text = await message(() => checkGithubToken(bad));
|
|
63
|
+
t.check(text.includes(expect) && !text.includes(bad) && !text.includes(bad.slice(4, 24)), why + ' is refused, and the refusal does not repeat it', text);
|
|
64
|
+
}
|
|
65
|
+
t.check((githubTokenProblem(OAUTH)?.fix ?? '').includes('personal-access-tokens/new'), 'a refused OAuth token says to create a fine-grained token instead');
|
|
66
|
+
t.check(SecretsFileSchema.safeParse({ githubToken: FINE }).success &&
|
|
67
|
+
!SecretsFileSchema.safeParse({ githubToken: 'a b' }).success &&
|
|
68
|
+
!SecretsFileSchema.safeParse({ githubToken: 'x'.repeat(256) }).success, 'the secrets file holds the token with the same limits');
|
|
69
|
+
t.section('the saved token');
|
|
70
|
+
const storage = t.dir('github-storage');
|
|
71
|
+
const store = new GithubToken(storage, SecretStore.memory());
|
|
72
|
+
t.check(store.state() === 'none' && store.kind() === null, 'nothing is saved at first');
|
|
73
|
+
await t.throws(() => store.save(OAUTH), 'OAuth', 'an OAuth token is not saved');
|
|
74
|
+
t.check(store.state() === 'none', 'and nothing was written');
|
|
75
|
+
t.check(store.save(FINE) === 'fine-grained', 'a fine-grained token is saved');
|
|
76
|
+
const file = path.join(storage, 'github', 'token.json');
|
|
77
|
+
t.check(store.state() === 'saved' && store.kind() === 'fine-grained' && !fs.readFileSync(file, 'utf8').includes(FINE), 'it is saved sealed: the token is not in the file');
|
|
78
|
+
t.check(store.read() === FINE, 'and it opens for the launch');
|
|
79
|
+
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/"sealed": "[^"]*"/, '"sealed": "dpapi1:AAAA"'));
|
|
80
|
+
t.check(store.state() === 'unreadable', 'a token this Windows user cannot open is reported as unreadable');
|
|
81
|
+
t.check(store.remove() && store.state() === 'none' && !store.remove(), 'logout deletes it (once)');
|
|
82
|
+
t.section('when a launch hands it over');
|
|
83
|
+
const config = f.makeConfig();
|
|
84
|
+
const tokens = new GithubToken(config.storage.root, SecretStore.memory());
|
|
85
|
+
t.check(DEFAULTS.github.shareToken === false, 'sharing is off by default');
|
|
86
|
+
t.equal(prepareGithubToken(config, config.storage.root, tokens), { notes: [] }, 'off: nothing, and nothing said');
|
|
87
|
+
config.github.shareToken = true;
|
|
88
|
+
const none = prepareGithubToken(config, config.storage.root, tokens);
|
|
89
|
+
t.check(none.token === undefined && none.notes.some((n) => n.includes('sandbox github login')), 'on without a saved token: not sent, and it says how to save one');
|
|
90
|
+
tokens.save(FINE);
|
|
91
|
+
for (const access of ['readonly', 'none']) {
|
|
92
|
+
const strict = structuredClone(config);
|
|
93
|
+
strict.provisioning.git.access = access;
|
|
94
|
+
const d = prepareGithubToken(strict, config.storage.root, tokens);
|
|
95
|
+
t.check(d.token === undefined && !githubTokenAllowed(strict) && d.notes.some((n) => n.includes('"' + access + '"')), 'git access ' + access + ': not sent, with a note why');
|
|
96
|
+
}
|
|
97
|
+
const unreadable = new GithubToken(t.dir('github-unreadable'), SecretStore.memory());
|
|
98
|
+
unreadable.save(FINE);
|
|
99
|
+
const uFile = path.join(t.at('github-unreadable'), 'github', 'token.json');
|
|
100
|
+
fs.writeFileSync(uFile, fs.readFileSync(uFile, 'utf8').replace(/"sealed": "[^"]*"/, '"sealed": "dpapi1:AAAA"'));
|
|
101
|
+
const u = prepareGithubToken(config, config.storage.root, unreadable);
|
|
102
|
+
t.check(u.token === undefined && u.notes.some((n) => n.includes('could not be read')), 'a token that cannot be decrypted is a warning, not a crash');
|
|
103
|
+
const all = prepareGithubToken(config, config.storage.root, tokens);
|
|
104
|
+
t.check(all.token === FINE && all.notes.every((n) => !n.includes(FINE)) && all.notes.some((n) => n.includes('can read')), 'all on: the token is handed over, the notes never hold it and say the agent can read it');
|
|
105
|
+
const run = await f.run({}, config);
|
|
106
|
+
clearSecrets(config.storage.root, run.dataPath);
|
|
107
|
+
const launched = generateSandbox({
|
|
108
|
+
packageRoot: t.packageRoot,
|
|
109
|
+
run,
|
|
110
|
+
harnessArgs: [],
|
|
111
|
+
probe: f.probe,
|
|
112
|
+
pcLanguages: ['python', 'rust', 'cpp', 'dotnet'],
|
|
113
|
+
githubToken: FINE,
|
|
114
|
+
});
|
|
115
|
+
const session = fs.readFileSync(launched.sessionPath, 'utf8');
|
|
116
|
+
t.check(!session.includes(FINE) && !allText(launched.launchPath).some((text) => text.includes(FINE)), 'the token is in neither session.json nor any file of the launch folder');
|
|
117
|
+
const runDir = new RunDir(run.dataPath);
|
|
118
|
+
const handed = f.readJson(runDir.secretFile(launched.launchId));
|
|
119
|
+
t.check(handed.githubToken === FINE, 'it is only in the one-shot file for the sandbox');
|
|
120
|
+
const runFolder = path.join(run.dataPath, 'run');
|
|
121
|
+
t.check(!fs.existsSync(runFolder) || !allText(runFolder).some((text) => text.includes(FINE)), 'and not in run\\');
|
|
122
|
+
clearSecrets(config.storage.root, run.dataPath);
|
|
123
|
+
const readonlyConfig = structuredClone(config);
|
|
124
|
+
readonlyConfig.provisioning.git.access = 'readonly';
|
|
125
|
+
const readonlyRun = await f.run({}, readonlyConfig);
|
|
126
|
+
const strictLaunch = generateSandbox({
|
|
127
|
+
packageRoot: t.packageRoot,
|
|
128
|
+
run: readonlyRun,
|
|
129
|
+
harnessArgs: [],
|
|
130
|
+
probe: f.probe,
|
|
131
|
+
pcLanguages: [],
|
|
132
|
+
githubToken: FINE,
|
|
133
|
+
});
|
|
134
|
+
const handedTo = (id) => fs.existsSync(runDir.secretFile(id)) ? f.readJson(runDir.secretFile(id)) : {};
|
|
135
|
+
t.check(handedTo(strictLaunch.launchId).githubToken === undefined, 'generateSandbox checks again: a readonly sandbox never gets it, whatever it was given');
|
|
136
|
+
clearSecrets(config.storage.root, run.dataPath);
|
|
137
|
+
const plain = generateSandbox({
|
|
138
|
+
packageRoot: t.packageRoot,
|
|
139
|
+
run: await f.run({ noAgent: true }, config),
|
|
140
|
+
harnessArgs: [],
|
|
141
|
+
probe: f.probe,
|
|
142
|
+
pcLanguages: [],
|
|
143
|
+
githubToken: FINE,
|
|
144
|
+
});
|
|
145
|
+
t.check(handedTo(plain.launchId).githubToken === FINE, 'a sandbox without an agent (no other secret to hand over) still gets the one-shot file with the token');
|
|
146
|
+
clearSecrets(config.storage.root, run.dataPath);
|
|
147
|
+
const strictOther = structuredClone(config);
|
|
148
|
+
strictOther.provisioning.git.access = 'readonly';
|
|
149
|
+
const others = [{ name: 'other-project', config: strictOther }];
|
|
150
|
+
const held = prepareGithubToken(config, config.storage.root, tokens, others);
|
|
151
|
+
t.check(held.token === undefined &&
|
|
152
|
+
!githubTokenAllowed(config, others) &&
|
|
153
|
+
held.notes.some((n) => n.includes('other-project')), 'another project mapped at boot with readonly git access holds the token back, and the note names it');
|
|
154
|
+
const withOther = generateSandbox({
|
|
155
|
+
packageRoot: t.packageRoot,
|
|
156
|
+
run: await f.run({ noAgent: true }, config),
|
|
157
|
+
harnessArgs: [],
|
|
158
|
+
probe: f.probe,
|
|
159
|
+
pcLanguages: [],
|
|
160
|
+
githubToken: FINE,
|
|
161
|
+
bootWants: { projects: others, keepOff: [] },
|
|
162
|
+
});
|
|
163
|
+
t.check(handedTo(withOther.launchId).githubToken === undefined, 'and generateSandbox checks the projects at boot again');
|
|
164
|
+
clearSecrets(config.storage.root, run.dataPath);
|
|
165
|
+
t.section("settings: a project's file cannot turn it on");
|
|
166
|
+
t.check(ConfigSchema.safeParse(DEFAULTS).success, 'the defaults are valid');
|
|
167
|
+
t.check(!ConfigSchema.safeParse({ ...DEFAULTS, github: { shareToken: true, token: FINE } }).success, 'there is no setting that holds a token');
|
|
168
|
+
t.check(!LocalOverridesSchema.safeParse({ github: { shareToken: true } }).success &&
|
|
169
|
+
!LocalOverridesSchema.safeParse({ github: { shareToken: false } }).success, "a project's .sandbox-config.json cannot set github at all");
|
|
170
|
+
await t.throws(() => normalizeOverrides({ github: { shareToken: true } }, f.workA, '.sandbox-config.json'), 'not valid', 'such a file is refused as invalid');
|
|
171
|
+
const mine = structuredClone(DEFAULTS);
|
|
172
|
+
t.check(applyOverrides(mine, normalizeOverrides({ readOnly: true }, f.workA, 'x')).github.shareToken === false, 'and other project settings leave it as you set it');
|
|
173
|
+
t.section('the shared-core operations');
|
|
174
|
+
const featureStore = new GithubToken(t.dir('github-feature'), SecretStore.memory());
|
|
175
|
+
const { io: loginIO, seen: loginSeen } = fakeIO([FINE]);
|
|
176
|
+
const saved = await saveGithubTokenFlow(loginIO, featureStore);
|
|
177
|
+
t.check(loginSeen.questions.length === 1 &&
|
|
178
|
+
loginSeen.questions[0].kind === 'secret' &&
|
|
179
|
+
featureStore.state() === 'saved' &&
|
|
180
|
+
loginSeen.logs.some((l) => l.text.includes('personal-access-tokens/new')) &&
|
|
181
|
+
loginSeen.logs.some((l) => l.text.includes('agent in the sandbox can read')), 'login shows the steps, asks exactly one hidden question, saves, and warns the agent can read it', JSON.stringify(loginSeen.questions));
|
|
182
|
+
t.check(!JSON.stringify(saved).includes(FINE) && !JSON.stringify(loginSeen).includes(FINE.slice(12)), 'nothing it returns or logs holds the token');
|
|
183
|
+
const { io: retryIO, seen: retrySeen } = fakeIO([OAUTH, CLASSIC]);
|
|
184
|
+
await saveGithubTokenFlow(retryIO, featureStore);
|
|
185
|
+
t.check(retrySeen.questions.length === 2 &&
|
|
186
|
+
featureStore.kind() === 'classic' &&
|
|
187
|
+
!JSON.stringify(retrySeen).includes(OAUTH.slice(4)) &&
|
|
188
|
+
retrySeen.logs.some((l) => l.text.includes('classic')), 'a refused token is asked again (the refusal never repeats it), and a classic one is saved with a warning');
|
|
189
|
+
const status = (await runDefinition('github.status', {
|
|
190
|
+
storageRoot: t.dir('github-status'),
|
|
191
|
+
dataPath: t.at('github-status', 'data'),
|
|
192
|
+
loaded: { config: { ...DEFAULTS, github: { shareToken: true } } },
|
|
193
|
+
}, {}, fakeIO().io));
|
|
194
|
+
t.check(status.shareToken && status.token === 'none' && status.kind === null, 'status reads the setting');
|
|
195
|
+
const sample = {
|
|
196
|
+
shareToken: true,
|
|
197
|
+
gitAccess: 'readonly',
|
|
198
|
+
token: 'saved',
|
|
199
|
+
kind: 'classic',
|
|
200
|
+
savedAt: '2026-10-18T10:00:00.000Z',
|
|
201
|
+
};
|
|
202
|
+
const lines = pageLines(githubPage(sample)).join('\n');
|
|
203
|
+
t.check(lines.includes('saved (encrypted)') &&
|
|
204
|
+
lines.includes('only handed over with "full"') &&
|
|
205
|
+
lines.includes('classic'), 'the status page says saved, the git access it needs, and the kind', lines);
|
|
206
|
+
t.equal(githubTokenRowOf({ ...sample, token: 'none' }).hint, 'not saved', 'the Settings row says not saved');
|
|
207
|
+
const { io: logoutIO, seen: logoutSeen } = fakeIO([true]);
|
|
208
|
+
const out = await deleteGithubTokenFlow(logoutIO, featureStore);
|
|
209
|
+
t.check(out.deleted &&
|
|
210
|
+
logoutSeen.questions.length === 1 &&
|
|
211
|
+
logoutSeen.questions[0].kind === 'confirm' &&
|
|
212
|
+
featureStore.state() === 'none', 'logout asks to confirm, then deletes');
|
|
213
|
+
const { io: noneIO, seen: noneSeen } = fakeIO();
|
|
214
|
+
t.check(!(await deleteGithubTokenFlow(noneIO, featureStore)).deleted && noneSeen.questions.length === 0, 'logout with nothing saved asks nothing');
|
|
215
|
+
const { io: inputIO, seen: inputSeen } = fakeIO();
|
|
216
|
+
await t.throws(() => runDefinition('github.login', {}, { token: FINE }, inputIO), 'does not work', 'the token is never accepted as an input');
|
|
217
|
+
t.check(inputSeen.questions.length === 0, 'a token passed on the command line asks nothing');
|
|
218
|
+
if (insideSandbox()) {
|
|
219
|
+
const { io, seen } = fakeIO();
|
|
220
|
+
await t.throws(() => runDefinition('github.login', {}, {}, io), 'inside the sandbox already', 'github login refuses inside the sandbox');
|
|
221
|
+
t.check(seen.questions.length === 0, 'and asks no question first');
|
|
222
|
+
}
|
|
223
|
+
t.section('gh in the sandbox: the token on stdin only');
|
|
224
|
+
const base = {
|
|
225
|
+
PATH: 'C:\\x',
|
|
226
|
+
GH_TOKEN: 'other',
|
|
227
|
+
github_token: 'other2',
|
|
228
|
+
GH_CONFIG_DIR: 'C:\\AgentData\\state\\gh',
|
|
229
|
+
};
|
|
230
|
+
const login = ghLoginCommand('C:\\tools\\gh.exe', 'C:\\nvm\\gh-token', FINE, base);
|
|
231
|
+
t.check(login.input === FINE + '\n' &&
|
|
232
|
+
!login.args.some((a) => a.includes(FINE)) &&
|
|
233
|
+
login.args.includes('--with-token') &&
|
|
234
|
+
!Object.values(login.env).some((v) => v.includes(FINE)), 'gh auth login gets the token on stdin, never in its arguments or environment', JSON.stringify(login.args));
|
|
235
|
+
t.check(login.env.GH_CONFIG_DIR === 'C:\\nvm\\gh-token' &&
|
|
236
|
+
!('GH_TOKEN' in login.env) &&
|
|
237
|
+
!('github_token' in login.env) &&
|
|
238
|
+
login.env.PATH === 'C:\\x', 'it uses the sandbox-only folder, and other tokens in the environment are dropped (gh would prefer them)');
|
|
239
|
+
const setup = ghSetupGitCommand('C:\\tools\\gh.exe', 'C:\\nvm\\gh-token', base);
|
|
240
|
+
t.check(setup.input === undefined && setup.args.join(' ') === 'auth setup-git --hostname github.com', 'setup-git gets nothing on stdin');
|
|
241
|
+
t.check(!ghOutputText('error: bad credentials ' + FINE, FINE).includes(FINE), 'what gh prints is logged with the token blanked out');
|
|
242
|
+
},
|
|
243
|
+
};
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { cleanEngineRun, engineLeftoversRun } from '../app/features/engine/index.js';
|
|
8
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
7
9
|
import { DEFAULTS } from '../core/config/index.js';
|
|
8
10
|
import { isWin } from '../core/util/index.js';
|
|
9
11
|
import { HCS_CHECKS, HCS_PROBES } from '../cli/commands/repair/checks/hcs.js';
|
|
10
|
-
import { checkHcs, engineFor, HCS_CAPABILITIES, HcsEngine, openEngine, } from '../host/engines/index.js';
|
|
12
|
+
import { checkHcs, engineFor, HCS_CAPABILITIES, HcsEngine, LEFTOVER_DEPS, lookForLeftovers, openEngine, stopLeftovers, } from '../host/engines/index.js';
|
|
11
13
|
import { parseWhoamiGroups } from '../host/engines/hcs/check.js';
|
|
12
14
|
import { buildDocuments, cpuCount, layerGuid } from '../host/engines/hcs/documents.js';
|
|
13
|
-
import { HcsHelper, HcsReplySchema, buildHcsHostTo } from '../host/engines/hcs/helper.js';
|
|
15
|
+
import { HcsHelper, HcsReplySchema, buildHcsHostTo, userSystemPrefix, } from '../host/engines/hcs/helper.js';
|
|
14
16
|
import { chainIds, DockerImageStore, FolderImageStore } from '../host/engines/hcs/images.js';
|
|
15
17
|
import { SYSTEM } from '../shared/system.js';
|
|
16
18
|
import { fakeProbes, FAKE_IMAGE } from './support/hcs-probes.js';
|
|
@@ -378,16 +380,18 @@ export const hcsSuite = {
|
|
|
378
380
|
});
|
|
379
381
|
for (const file of plan.files)
|
|
380
382
|
t.write(file.path, file.text);
|
|
383
|
+
const mineId = userSystemPrefix() + '1';
|
|
381
384
|
const running = await live.start(plan);
|
|
382
385
|
t.equal(log, [
|
|
383
|
-
'grant-vm-access
|
|
384
|
-
'grant-vm-access
|
|
386
|
+
'grant-vm-access ' + mineId + ' C:\\S\\images\\set.vhdx',
|
|
387
|
+
'grant-vm-access ' + mineId + ' ' + path.join(launch.launchPath, 'hcs', 'scratch.vhdx'),
|
|
385
388
|
'create <request>',
|
|
386
|
-
'start
|
|
389
|
+
'start ' + mineId,
|
|
387
390
|
'create <request>',
|
|
388
|
-
'start
|
|
391
|
+
'start ' + mineId + '-c',
|
|
389
392
|
], 'disks get access, the utility VM is created and started, then the container');
|
|
390
|
-
t.check(running.id ===
|
|
393
|
+
t.check(running.id === mineId && running.engine === 'hcs', 'start() reports the sandbox');
|
|
394
|
+
t.check(mineId !== userSystemPrefix('someone-else', 'PC') + '1', "the id carries this Windows user's tag (another user's sandbox of the same name gets another id)");
|
|
391
395
|
t.section('the helper: private temp folder, checked answers');
|
|
392
396
|
const tempRoot = t.dir('hcs-helper-checks');
|
|
393
397
|
const answers = [];
|
|
@@ -416,6 +420,99 @@ export const hcsSuite = {
|
|
|
416
420
|
HCS_PROBES.current = null;
|
|
417
421
|
t.check(finding.status === 'needs-you' && /docker pull/.test(finding.fix ?? ''), 'the row says what is missing and the command, and repair has no automatic fix for it');
|
|
418
422
|
t.check(row.fix === undefined, 'repair never fixes HCS prerequisites (they change Windows)');
|
|
423
|
+
t.section('HCS systems left running: only ours are listed and stopped (fake helper)');
|
|
424
|
+
const mine = userSystemPrefix() + '1';
|
|
425
|
+
const theirs = userSystemPrefix('someone-else', 'PC') + '1';
|
|
426
|
+
const systems = [
|
|
427
|
+
{
|
|
428
|
+
Id: mine,
|
|
429
|
+
SystemType: 'VirtualMachine',
|
|
430
|
+
Owner: 'agent-sandbox',
|
|
431
|
+
State: 'Running',
|
|
432
|
+
CreateTimestamp: '2026-01-01T10:00:00Z',
|
|
433
|
+
},
|
|
434
|
+
{ Id: mine + '-c', SystemType: 'Container', Owner: 'agent-sandbox', State: 'Running' },
|
|
435
|
+
{ Id: theirs, SystemType: 'VirtualMachine', Owner: 'agent-sandbox', State: 'Running' },
|
|
436
|
+
{ Id: 'WindowsSandbox-9', SystemType: 'VirtualMachine', Owner: 'agent-sandbox', State: 'Running' },
|
|
437
|
+
{ Id: 'agent-sandbox-evil', SystemType: 'VirtualMachine', Owner: 'docker', State: 'Running' },
|
|
438
|
+
{ Id: 'docker-1', SystemType: 'Container', Owner: 'docker', State: 'Running' },
|
|
439
|
+
];
|
|
440
|
+
const calls = [];
|
|
441
|
+
let present = [...systems];
|
|
442
|
+
const leftoverRun = (_exe, args) => {
|
|
443
|
+
const verb = args[0];
|
|
444
|
+
calls.push(args.slice(0, args.indexOf('--out')).join(' '));
|
|
445
|
+
if (verb === 'terminate')
|
|
446
|
+
present = present.filter((s) => s.Id !== args[1]);
|
|
447
|
+
const json = verb === 'enumerate' ? JSON.stringify(present) : '';
|
|
448
|
+
fs.writeFileSync(args[args.indexOf('--out') + 1], JSON.stringify({ ok: true, hresult: '0x0', message: 'ok', json, values: {} }), 'utf8');
|
|
449
|
+
return { status: 0 };
|
|
450
|
+
};
|
|
451
|
+
const leftoverTemp = t.dir('hcs-leftovers-temp');
|
|
452
|
+
const helperCalls = [];
|
|
453
|
+
const useLeftovers = (probes = fakeProbes()) => {
|
|
454
|
+
LEFTOVER_DEPS.current = {
|
|
455
|
+
probes,
|
|
456
|
+
helper: () => {
|
|
457
|
+
helperCalls.push(true);
|
|
458
|
+
return new HcsHelper(() => 'C:\\fake\\HcsHost.exe', leftoverRun, leftoverTemp);
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
const ctx = { loaded: { config: hcsConfig } };
|
|
463
|
+
try {
|
|
464
|
+
useLeftovers();
|
|
465
|
+
const look = lookForLeftovers('C:\\S');
|
|
466
|
+
t.check(look.available &&
|
|
467
|
+
look.systems.map((s) => s.id).join() === mine + ',' + mine + '-c' &&
|
|
468
|
+
look.systems[0].kind === 'VirtualMachine' &&
|
|
469
|
+
look.systems[0].since === '2026-01-01T10:00:00Z' &&
|
|
470
|
+
look.systems[1].since === null, "only systems with our Owner and this user's id prefix are listed (not another user's, Windows Sandbox, Docker, or a foreign owner)", JSON.stringify(look));
|
|
471
|
+
const query = await engineLeftoversRun.run(ctx, {}, fakeIO().io);
|
|
472
|
+
t.check(query.available && query.systems.length === 2 && /2 HCS systems/.test(query.message), 'engine.leftovers returns them with a plain message');
|
|
473
|
+
calls.length = 0;
|
|
474
|
+
const outcome = stopLeftovers('C:\\S', [mine, mine + '-c', theirs, 'WindowsSandbox-9', 'docker-1', '--evil']);
|
|
475
|
+
t.equal(calls.filter((c) => c.startsWith('terminate')), ['terminate ' + mine + '-c', 'terminate ' + mine], 'only ours reach terminate, the container before its utility VM');
|
|
476
|
+
t.check(outcome.filter((o) => o.stopped).length === 2 && outcome.filter((o) => !o.stopped).length === 4, 'ids that are not ours (or not running) are reported, never terminated');
|
|
477
|
+
present = [...systems];
|
|
478
|
+
calls.length = 0;
|
|
479
|
+
const declined = fakeIO([false]);
|
|
480
|
+
await t.throws(() => cleanEngineRun.run(ctx, {}, declined.io), 'Nothing changed', 'declining the question stops cleanly');
|
|
481
|
+
t.check(!calls.some((c) => c.startsWith('terminate')), 'declining terminates nothing');
|
|
482
|
+
calls.length = 0;
|
|
483
|
+
const yes = fakeIO([true]);
|
|
484
|
+
const cleaned = await cleanEngineRun.run(ctx, {}, yes.io);
|
|
485
|
+
t.check(cleaned.stopped === 2 &&
|
|
486
|
+
cleaned.failed === 0 &&
|
|
487
|
+
yes.seen.questions.length === 1 &&
|
|
488
|
+
yes.seen.questions[0].kind === 'confirm' &&
|
|
489
|
+
calls.filter((c) => c.startsWith('terminate')).length === 2, 'engine.clean asks once, then stops what is ours');
|
|
490
|
+
const again = await cleanEngineRun.run(ctx, {}, fakeIO().io);
|
|
491
|
+
t.check(again.stopped === 0 && /Nothing to clean/.test(again.message), 'with none left it asks nothing and says so');
|
|
492
|
+
calls.length = 0;
|
|
493
|
+
helperCalls.length = 0;
|
|
494
|
+
useLeftovers(fakeProbes({ computecore: false }));
|
|
495
|
+
const missing = await cleanEngineRun.run(ctx, {}, fakeIO().io);
|
|
496
|
+
t.check(!missing.available && /not on this PC/.test(missing.message) && calls.length === 0 && helperCalls.length === 0, 'HCS missing: a plain message, the helper is never started, nothing to clean');
|
|
497
|
+
t.section('doctor row: HCS systems left running');
|
|
498
|
+
present = [...systems];
|
|
499
|
+
useLeftovers();
|
|
500
|
+
const leftoverRow = HCS_CHECKS[1];
|
|
501
|
+
t.check(leftoverRow.name === 'HCS systems left running' &&
|
|
502
|
+
leftoverRow.applies?.(rowEnv(f.baseConfig)) === true &&
|
|
503
|
+
helperCalls.every(Boolean), 'shown on any engine when one of ours is found, and the doctor never builds HcsHost.exe to look');
|
|
504
|
+
const warn = await leftoverRow.run(rowEnv(f.baseConfig));
|
|
505
|
+
t.check(warn.status === 'warn' && warn.fix === 'sandbox engine clean' && warn.detail.includes(mine), 'a warning with the fix "sandbox engine clean"');
|
|
506
|
+
present = [];
|
|
507
|
+
t.check(leftoverRow.applies?.(rowEnv(f.baseConfig)) === false &&
|
|
508
|
+
leftoverRow.applies?.(rowEnv(hcsConfig)) === true &&
|
|
509
|
+
(await leftoverRow.run(rowEnv(hcsConfig))).status === 'ok', 'hidden when none is left (unless the engine is hcs, where it says none is running)');
|
|
510
|
+
useLeftovers(fakeProbes({ computecore: false }));
|
|
511
|
+
t.check(leftoverRow.applies?.(rowEnv(f.baseConfig)) === false, 'hidden on a PC without HCS');
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
LEFTOVER_DEPS.current = null;
|
|
515
|
+
}
|
|
419
516
|
t.section('sandbox engine check hcs');
|
|
420
517
|
const out = await f.runBin('agent-sandbox', ['engine', 'check', 'hcs', '--config', t.writeJson(t.at('hcs-config.json'), {})], {
|
|
421
518
|
cwd: f.workA,
|
|
@@ -425,7 +522,7 @@ export const hcsSuite = {
|
|
|
425
522
|
(out.status === 0 || out.out.includes('To fix:')), 'the command prints the check in plain words with the fixes', out.out);
|
|
426
523
|
t.check(DEFAULTS.sandbox.engine === 'windows-sandbox', 'the default engine is unchanged');
|
|
427
524
|
t.section("documents against Microsoft's published HCS schema (Windows 11 21H2 and Windows 10 1809)");
|
|
428
|
-
const schemaDir = path.join(t.packageRoot, '
|
|
525
|
+
const schemaDir = path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'hcs-schema');
|
|
429
526
|
if (!fs.existsSync(schemaDir)) {
|
|
430
527
|
t.check(true, 'skipped: the schema fixtures are only in the repository');
|
|
431
528
|
}
|
|
@@ -5,6 +5,10 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { ALL_CHECKS } from '../cli/commands/repair/checks/index.js';
|
|
7
7
|
import { proxyProblem } from '../cli/commands/repair/checks/network.js';
|
|
8
|
+
import { healthPage, ioEvents, summaryText as featureSummaryText, } from '../app/features/health/index.js';
|
|
9
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
10
|
+
import { runDefinition } from '../app/registry.js';
|
|
11
|
+
import { menuView } from '../cli/menu/render-view.js';
|
|
8
12
|
import { HealthRun, summarize } from '../cli/commands/repair/engine.js';
|
|
9
13
|
import { logText, writeRepairLog } from '../cli/commands/repair/log.js';
|
|
10
14
|
import { HealthPrinter, summaryText } from '../cli/commands/repair/printer.js';
|
|
@@ -455,6 +459,72 @@ function backups(t) {
|
|
|
455
459
|
t.equal(found[0].label, '1.4.0 to 1.5.0 at 2026-10-14 09:30:12', 'a backup is labelled with its versions and time');
|
|
456
460
|
t.equal(lineDiff('{\n "a": 1,\n "x": 3\n}', '{\n "a": 2,\n "x": 3\n}'), ['- "a": 1,', '+ "a": 2,'], 'the difference shows the lines only in one file');
|
|
457
461
|
}
|
|
462
|
+
/** The shared core: the same run as the menu and the web use, the saved results, and the health page. */
|
|
463
|
+
async function sharedCore(t) {
|
|
464
|
+
t.section('the shared core');
|
|
465
|
+
const bad = (plan, over = {}) => ({
|
|
466
|
+
status: 'needs-you',
|
|
467
|
+
detail: 'broken',
|
|
468
|
+
plan,
|
|
469
|
+
means: 'it hurts',
|
|
470
|
+
...over,
|
|
471
|
+
});
|
|
472
|
+
const cracks = [];
|
|
473
|
+
const three = [
|
|
474
|
+
check('p1', 'This PC', { name: 'Windows' }),
|
|
475
|
+
check('p2', 'Data folder', {
|
|
476
|
+
name: 'Cracks',
|
|
477
|
+
run: () => (cracks.length ? { status: 'ok', detail: 'whole' } : bad('mend the cracks', { fix: undefined })),
|
|
478
|
+
fix: () => {
|
|
479
|
+
cracks.push('mended');
|
|
480
|
+
return { done: 'mended the cracks' };
|
|
481
|
+
},
|
|
482
|
+
}),
|
|
483
|
+
check('p3', 'Network', {
|
|
484
|
+
name: 'GitHub',
|
|
485
|
+
run: () => ({ status: 'warn', detail: 'no answer', fix: 'check your VPN' }),
|
|
486
|
+
}),
|
|
487
|
+
];
|
|
488
|
+
const fake = fakeIO();
|
|
489
|
+
await runHealth(fakeEnv(t, 'events'), ioEvents(fake.io), { yes: true, ask: false, question: () => '' }, three, () => Promise.resolve(true));
|
|
490
|
+
const logged = fake.seen.logs.map((l) => l.text).join('\n');
|
|
491
|
+
t.includes(logged, ' [ok] Windows: p1 is fine', 'ioEvents writes a line per result, like the printer');
|
|
492
|
+
t.includes(logged, 'What it means: it hurts', 'ioEvents says what a problem means');
|
|
493
|
+
t.includes(logged, 'Fix: sandbox repair will mend the cracks', 'ioEvents says what repair would do');
|
|
494
|
+
t.includes(logged, 'Repair will change:', 'ioEvents lists the changes before they are made');
|
|
495
|
+
t.includes(logged, '[fixed] Cracks: mended the cracks', 'ioEvents reports a fix on its own line');
|
|
496
|
+
t.equal(featureSummaryText({ total: 9, ok: 6, fixed: 1, warn: 1, needsYou: 1, skipped: 0, fixable: 0 }), '6 ok, 1 fixed, 1 needs you, 1 warning', 'summaryText moved to the feature and reads the same');
|
|
497
|
+
t.equal(featureSummaryText({ total: 3, ok: 0, fixed: 0, warn: 0, needsYou: 2, skipped: 1, fixable: 0 }), '0 ok, 2 need you, 1 skipped', 'summaryText plurals (feature)');
|
|
498
|
+
const saved = {
|
|
499
|
+
at: '2026-10-15T09:30:12.000Z',
|
|
500
|
+
mode: 'check',
|
|
501
|
+
checks: [{ id: 'p1', group: 'This PC', name: 'Windows', status: 'ok', detail: 'fine', fixable: false }],
|
|
502
|
+
summary: { total: 1, ok: 1, fixed: 0, warn: 0, needsYou: 0, skipped: 0, fixable: 0 },
|
|
503
|
+
details: [['Config file', 'valid']],
|
|
504
|
+
setup: { checks: [{ name: 'Edition', status: 'ok', detail: 'Pro' }], canEnable: false, error: null },
|
|
505
|
+
log: null,
|
|
506
|
+
};
|
|
507
|
+
const storage = t.dir('health', 'saved', 'storage');
|
|
508
|
+
const ctx = { storageRoot: storage };
|
|
509
|
+
const none = await runDefinition('health.last', ctx, {}, fakeIO().io);
|
|
510
|
+
t.equal(none, { last: null }, 'nothing saved yet: no results');
|
|
511
|
+
t.writeJson(path.join(storage, 'health', 'last.json'), saved);
|
|
512
|
+
const read = await runDefinition('health.last', ctx, {}, fakeIO().io);
|
|
513
|
+
t.equal(read, { last: saved }, 'a check is saved and read back by health.last');
|
|
514
|
+
t.write(path.join(storage, 'health', 'last.json'), '{ this is not json');
|
|
515
|
+
t.equal(await runDefinition('health.last', ctx, {}, fakeIO().io), { last: null }, 'a damaged results file reads as nothing saved');
|
|
516
|
+
const empty = healthPage({ last: null });
|
|
517
|
+
const emptyIds = empty.sections.flatMap((s) => (s.items ?? []).map((i) => i.id));
|
|
518
|
+
t.equal(emptyIds, ['health:check', 'repair:run'], 'before any check the page offers the check and the repair');
|
|
519
|
+
t.equal(menuView(empty, { category: 'health', refresh: false }).enterLabel('health', 'repair:run'), 'check and fix', 'Enter on the repair row says check and fix');
|
|
520
|
+
const page = healthPage({ last: saved });
|
|
521
|
+
const ids = page.sections.flatMap((s) => (s.items ?? []).map((i) => i.id));
|
|
522
|
+
t.check(['doctor:0', 'repair:0', 'setup:0'].every((id) => ids.includes(id)), 'with results the page lists the doctor, repair and setup rows', ids.join(', '));
|
|
523
|
+
const keys = menuView(page, { category: 'health', refresh: false }).actions.map((a) => a.key);
|
|
524
|
+
t.equal(keys, ['f', 'l'], 'the health keys are f and l (e only when Windows Sandbox can be turned on)');
|
|
525
|
+
const canEnable = healthPage({ last: { ...saved, setup: { ...saved.setup, canEnable: true } } });
|
|
526
|
+
t.equal(menuView(canEnable, { category: 'health', refresh: false }).actions.map((a) => a.key), ['f', 'l', 'e'], 'e turns Windows Sandbox on when the setup says it can be');
|
|
527
|
+
}
|
|
458
528
|
export const healthSuite = {
|
|
459
529
|
name: 'health',
|
|
460
530
|
title: 'doctor and repair (engine, printer, log, the install, stale state, permissions, settings backups)',
|
|
@@ -466,5 +536,6 @@ export const healthSuite = {
|
|
|
466
536
|
await staleState(t);
|
|
467
537
|
parsing(t);
|
|
468
538
|
backups(t);
|
|
539
|
+
await sharedCore(t);
|
|
469
540
|
},
|
|
470
541
|
};
|