windows-development-sandbox 1.9.0 → 1.11.3
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 +7 -2
- 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 +201 -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 +952 -4
- package/dist/app/features/defs.js +147 -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 +281 -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 +225 -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 +225 -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 +23 -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 -88
- package/dist/cli/commands/web.d.ts +7 -1
- package/dist/cli/commands/web.js +45 -71
- 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 +3 -2
- package/dist/core/schema/config.d.ts +6 -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/schema/vpn.js +1 -1
- package/dist/core/schema/web.d.ts +1 -0
- package/dist/core/schema/web.js +4 -1
- package/dist/core/types.d.ts +6 -3
- package/dist/guest/boot/boot.js +11 -1
- 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 +53 -15
- package/dist/guest/governor/decide.js +155 -72
- package/dist/guest/governor/governor.d.ts +2 -35
- package/dist/guest/governor/governor.js +35 -99
- package/dist/guest/governor/index.d.ts +1 -0
- package/dist/guest/governor/index.js +1 -0
- package/dist/guest/network/firewall.d.ts +3 -0
- package/dist/guest/network/firewall.js +65 -23
- package/dist/guest/runtime/process.js +53 -5
- package/dist/guest/sessions/viewer.js +5 -1
- package/dist/guest/setup/claude-mem.js +12 -4
- 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 +40 -4
- 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 +16 -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 +1 -0
- package/dist/host/life/host-memory.js +4 -1
- package/dist/host/life/watchdog.d.ts +11 -0
- package/dist/host/life/watchdog.js +36 -4
- 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 +44 -16
- package/dist/runtime/daemon.js +1154 -857
- package/dist/runtime/db.js +44 -16
- package/dist/runtime/main.js +532 -115
- package/dist/runtime/netproxy.js +34 -9
- package/dist/runtime/view.js +499 -149
- package/dist/selftest/app.test.js +620 -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/guest-boot.test.js +47 -1
- package/dist/selftest/hcs.test.js +105 -8
- package/dist/selftest/health.test.js +71 -0
- package/dist/selftest/heavy-load.test.js +152 -66
- 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/network.test.js +20 -1
- 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 +5 -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/io/copy-tree.d.ts +7 -0
- package/dist/shared/io/copy-tree.js +45 -0
- package/dist/shared/io/index.d.ts +2 -0
- package/dist/shared/io/index.js +2 -0
- package/dist/shared/io/robocopy-result.d.ts +6 -0
- package/dist/shared/io/robocopy-result.js +23 -0
- package/dist/shared/memory/policy.d.ts +38 -12
- package/dist/shared/memory/policy.js +58 -19
- package/dist/shared/network/firewall-rules.d.ts +8 -0
- package/dist/shared/network/firewall-rules.js +28 -0
- 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.js +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/{main-T5DMCHYV.css → main-6JF3ASB5.css} +1 -1
- package/dist/web/main-Z645W45E.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 +128 -107
- package/native/scripts/memory-status.ps1 +10 -2
- package/package.json +10 -3
- package/sandbox/csharp/MemGov.cs +668 -398
- 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/new-shortcuts.ps1 +3 -1
- package/sandbox/scripts/repair-wmi-services.ps1 +26 -0
- package/schemas/config.schema.json +25 -7
- 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
|
@@ -5,7 +5,9 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { parseCli } from '../cli/args.js';
|
|
8
|
-
import { describeResult
|
|
8
|
+
import { describeResult } from '../app/features/updates/update-lines.js';
|
|
9
|
+
import { updateWith } from '../app/features/updates/updates.run.js';
|
|
10
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
9
11
|
import { UpdateService, runsAsSystem } from '../cli/services/index.js';
|
|
10
12
|
import { InstallLayout, InstallRecordFile, UpdateTask, shimText } from '../host/install/index.js';
|
|
11
13
|
import { nativeScript } from '../host/native.js';
|
|
@@ -170,7 +172,7 @@ async function failedInstall(t) {
|
|
|
170
172
|
t.check(bad2?.kind === 'failed' &&
|
|
171
173
|
bad2.autoUpdateOff &&
|
|
172
174
|
new InstallRecordFile(root2).read()?.autoUpdate === false &&
|
|
173
|
-
(await
|
|
175
|
+
(await updateWith(svc2, { check: false, rollback: false, unattended: true }, fakeIO().io)).line === '' &&
|
|
174
176
|
layout2.activeVersion() === '1.0.0', 'a task that cannot be removed does not stop the rollback; an unattended run with updates off does nothing');
|
|
175
177
|
publish(t, feed2, '1.0.2', 'good');
|
|
176
178
|
const ok2 = await svc2.run();
|
|
@@ -204,11 +206,17 @@ function pure(t) {
|
|
|
204
206
|
async function command(t) {
|
|
205
207
|
t.section('sandbox update: what it prints');
|
|
206
208
|
const lines = [];
|
|
207
|
-
|
|
209
|
+
// sandbox update's work through the feature: the line it returns is what the command prints.
|
|
210
|
+
const runU = async (svc, input) => {
|
|
211
|
+
const auto = input.auto ? { auto: input.auto } : {};
|
|
212
|
+
const out = await updateWith(svc, { check: input.check ?? false, rollback: false, unattended: false, ...auto }, fakeIO([true]).io);
|
|
213
|
+
lines.push(out.line);
|
|
214
|
+
return out.line;
|
|
215
|
+
};
|
|
208
216
|
const npm = new UpdateService(null, { version: '1.0.0' });
|
|
209
217
|
const npmAuto = await t.thrownMessage(() => npm.setAuto(true));
|
|
210
218
|
t.check(!npm.managed &&
|
|
211
|
-
(await
|
|
219
|
+
(await runU(npm, {})).includes('npm install -g windows-development-sandbox') &&
|
|
212
220
|
lines.join('\n').includes('npm install -g windows-development-sandbox') &&
|
|
213
221
|
npmAuto.includes('installed with npm'), 'an npm install is not managed, is told the npm command, and cannot turn on automatic updates', npmAuto);
|
|
214
222
|
const root = t.dir('cmd', 'install');
|
|
@@ -221,17 +229,16 @@ async function command(t) {
|
|
|
221
229
|
sandboxRunning: () => false,
|
|
222
230
|
version: '1.0.0',
|
|
223
231
|
});
|
|
224
|
-
await
|
|
232
|
+
await runU(machine, { auto: 'on' });
|
|
225
233
|
const afterOn = [...task.calls];
|
|
226
|
-
await
|
|
234
|
+
await runU(machine, { auto: 'off' });
|
|
227
235
|
t.check(afterOn.join() === 'register' && task.calls.join() === 'register,remove' && !machine.autoUpdateDue(), '--auto on/off creates and removes the scheduled task of an all-users install; it never updates from a CLI start');
|
|
228
236
|
task.fail = true;
|
|
229
237
|
const noAdmin = await t.thrownMessage(() => machine.setAuto(true));
|
|
230
238
|
t.check(noAdmin.includes('needs admin') && new InstallRecordFile(root).read()?.autoUpdate === false, 'a task that cannot be created fails and the setting stays off', noAdmin);
|
|
231
239
|
publish(t, feed, '1.0.1', 'good');
|
|
232
240
|
lines.length = 0;
|
|
233
|
-
t.check((await
|
|
234
|
-
lines.join('\n').includes('Version 1.0.1 is available'), '--check names the newer version and exits 0');
|
|
241
|
+
t.check((await runU(machine, { check: true })).includes('Version 1.0.1 is available'), '--check names the newer version and exits 0');
|
|
235
242
|
const waiting = { kind: 'skipped', reason: 'sandbox-running', current: '1.0.0' };
|
|
236
243
|
t.check(describeResult(null, false).code === 0 &&
|
|
237
244
|
describeResult(waiting, false).code === 1 &&
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
// the settings rules for projects, and that the login never lands in session.json or the launch folder.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { vpnLoginRowOf, vpnPage, VPN_WARNINGS } from '../app/features/vpn/index.js';
|
|
6
|
+
import { fakeIO, pageActions } from '../app/kernel/index.js';
|
|
7
|
+
import { runDefinition } from '../app/registry.js';
|
|
8
|
+
import { pageLines } from '../cli/output/index.js';
|
|
9
|
+
import { VpnService } from '../cli/services/index.js';
|
|
5
10
|
import { DEFAULTS } from '../core/config/index.js';
|
|
11
|
+
import { insideSandbox } from '../host/sandbox/platform.js';
|
|
6
12
|
import { applyOverrides, loosenedRequests, NEVER_LOOSER, normalizeOverrides } from '../core/local-config/index.js';
|
|
7
13
|
import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
|
|
8
14
|
import { SecretStore } from '../host/secrets/index.js';
|
|
@@ -11,7 +17,7 @@ import { generateSandbox } from '../host/launch/index.js';
|
|
|
11
17
|
import { findCountry, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
|
|
12
18
|
import { RunDir } from '../shared/channel/index.js';
|
|
13
19
|
import { OPENVPN_MSI, WIREGUARD_MSI } from '../shared/vpn/download.js';
|
|
14
|
-
import { cleanServers, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
|
|
20
|
+
import { cleanServers, ipv4Except, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
|
|
15
21
|
import { assertSafeOvpn, inspectOvpn, PROGRAM_DIRECTIVES } from '../shared/vpn/ovpn.js';
|
|
16
22
|
import { assertSafeWireguard, handshakeDone, inspectWireguard, renderWireguard, serviceRunning, WIREGUARD_PROGRAM_KEYS, wireguardEndpoints, wireguardTunnelAddress, } from '../shared/vpn/wireguard.js';
|
|
17
23
|
const PROFILE = [
|
|
@@ -128,6 +134,32 @@ export const vpnSuite = {
|
|
|
128
134
|
c.includes('program=C:\\AgentLaunch\\ptyhost.exe')) &&
|
|
129
135
|
phase1.some((c) => c.includes('name=AgentSandbox-VPN-DHCP')), 'it allows loopback, each VPN server endpoint, the attach channel on the Hyper-V network and DHCP');
|
|
130
136
|
t.check(!phase1.some((c) => c.includes('localip=') || (c.includes('action=allow') && c.includes('remoteip=any'))), 'nothing else is allowed before the tunnel exists (no blanket rule)');
|
|
137
|
+
const blockAt = (name) => phase1.findIndex((c) => c.includes('name=AgentSandbox-VPN-' + name + ' '));
|
|
138
|
+
const internet = phase1[blockAt('Block-Internet')] ?? '';
|
|
139
|
+
t.check(['Block-Internet', 'Block-IPv6', 'Block-DNS-UDP', 'Block-DNS-TCP'].every((name) => blockAt(name) >= 0 && blockAt(name) < policyAt && phase1[blockAt(name)].includes('action=block')), 'the kill switch is made of block rules, added before anything else (Windows Sandbox allows all outbound itself)', phase1.join('\n'));
|
|
140
|
+
t.check(internet.includes('1.0.0.0-9.255.255.255') &&
|
|
141
|
+
internet.includes('-185.1.2.2,185.1.2.4-185.9.9.8,185.9.9.10-') &&
|
|
142
|
+
!/(^|[=,])10\.\d/.test(internet.split('remoteip=')[1] ?? '') &&
|
|
143
|
+
internet.includes('-172.15.255.255,172.32.0.0-') &&
|
|
144
|
+
internet.includes('-192.167.255.255,192.169.0.0-') &&
|
|
145
|
+
internet.endsWith('-223.255.255.255'), 'the block covers every public address except the VPN servers (private, loopback and multicast are the LAN block)', internet);
|
|
146
|
+
t.check(phase1[blockAt('Block-IPv6')].includes('remoteip=2000::/3') &&
|
|
147
|
+
phase1[blockAt('Block-DNS-UDP')].includes('protocol=udp remoteport=53') &&
|
|
148
|
+
phase1[blockAt('Block-DNS-TCP')].includes('protocol=tcp remoteport=53'), 'global IPv6 and DNS (which would leak the names you visit) are blocked too');
|
|
149
|
+
t.equal([
|
|
150
|
+
ipv4Except([]),
|
|
151
|
+
ipv4Except([[0, 0]]),
|
|
152
|
+
ipv4Except([[4294967295, 4294967295]]),
|
|
153
|
+
ipv4Except([
|
|
154
|
+
[5, 9],
|
|
155
|
+
[7, 12],
|
|
156
|
+
]),
|
|
157
|
+
], [
|
|
158
|
+
'0.0.0.0-255.255.255.255',
|
|
159
|
+
'0.0.0.1-255.255.255.255',
|
|
160
|
+
'0.0.0.0-255.255.255.254',
|
|
161
|
+
'0.0.0.0-0.0.0.4,0.0.0.13-255.255.255.255',
|
|
162
|
+
], 'address lists without some addresses (edges and overlaps)');
|
|
131
163
|
t.check(killSwitchCommands([], null).every((c) => !c.join(' ').includes('Server-1') || c[2] === 'delete') &&
|
|
132
164
|
!killSwitchCommands([], null).some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Host') && c[2] === 'add'), 'with no servers and no session host only the block and the loopback rules remain (the failure case: offline)');
|
|
133
165
|
t.equal(tunnelCommands('10.8.0.6')
|
|
@@ -135,9 +167,13 @@ export const vpnSuite = {
|
|
|
135
167
|
.filter((c) => c.includes(' add ')), [
|
|
136
168
|
'advfirewall firewall add rule name=AgentSandbox-VPN-Tunnel dir=out action=allow enable=yes profile=any protocol=any localip=10.8.0.6',
|
|
137
169
|
], 'once the tunnel is up, traffic from its own address (the tunnel) is allowed');
|
|
170
|
+
const phase2 = tunnelCommands('10.8.0.6').map((c) => c.join(' '));
|
|
171
|
+
t.check(['Block-Internet', 'Block-DNS-UDP', 'Block-DNS-TCP'].every((name) => phase2.some((c) => c.startsWith('advfirewall firewall set rule name=AgentSandbox-VPN-' + name + ' new ') &&
|
|
172
|
+
c.endsWith('localip=0.0.0.0-10.8.0.5,10.8.0.7-255.255.255.255'))) && !phase2.some((c) => c.includes('delete') && c.includes('Block-')), 'the block rules are narrowed in place to everything but the tunnel (never deleted, so no moment without them)', phase2.join('\n'));
|
|
138
173
|
await t.throws(() => tunnelCommands('10.8.0.6; calc'), 'Not an IPv4', 'a tunnel address that is not an address makes no rule');
|
|
139
174
|
t.check(removeKillSwitchCommands().some((c) => c.join(' ').includes('blockinbound,allowoutbound')) &&
|
|
140
|
-
removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Tunnel'))
|
|
175
|
+
removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Tunnel')) &&
|
|
176
|
+
removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Block-Internet')), 'taking it away restores the default and deletes the rules');
|
|
141
177
|
t.equal([
|
|
142
178
|
tunnelAddressFromLog('... net_addr_v4_add: 10.100.0.2/16 dev Wintun ...'),
|
|
143
179
|
tunnelAddressFromLog('ifconfig 10.8.0.6 255.255.255.0'),
|
|
@@ -288,6 +324,10 @@ export const vpnSuite = {
|
|
|
288
324
|
status('failed', { message: 'login refused' }).text.includes('no internet') &&
|
|
289
325
|
vpnLine({ state: 'failed', message: 'x', killSwitch: false, updatedAt: 'now' }).text.includes('normal connection') &&
|
|
290
326
|
status('starting').level === 'warn', "up with another address is fine, up with this PC's own address is flagged, a failure says what the kill switch did");
|
|
327
|
+
t.check(status('failed', { killSwitchHeld: false }).text.includes('ONLINE') &&
|
|
328
|
+
!status('failed', { killSwitchHeld: false }).text.includes('no internet') &&
|
|
329
|
+
status('failed', { killSwitchHeld: true }).text.includes('no internet') &&
|
|
330
|
+
status('up', { publicIp: '185.1.2.3', killSwitchHeld: false }).level === 'warn', 'a kill switch that did not hold is never reported as "no internet" (a failure says the sandbox is online)');
|
|
291
331
|
t.check(!vpnLine({
|
|
292
332
|
state: 'failed',
|
|
293
333
|
message: 'bad \u001b]52;c;ZXZpbA==\u0007 text',
|
|
@@ -408,5 +448,49 @@ export const vpnSuite = {
|
|
|
408
448
|
clearSecrets(wgSettings.storage.root, wgRun.dataPath);
|
|
409
449
|
t.check(!fs.existsSync(new RunDir(wgRun.dataPath).secretFile(wgLaunch.launchId)) &&
|
|
410
450
|
vpnNotes(wgSettings.vpn).some((n) => n.includes('WireGuard private key') && n.includes('turn the tunnel off')), 'the launch warns that the agent can read the key and turn the tunnel off');
|
|
451
|
+
t.section('the VPN feature: status page, login and logout');
|
|
452
|
+
const sample = {
|
|
453
|
+
enabled: true,
|
|
454
|
+
provider: 'nordvpn',
|
|
455
|
+
protocol: 'udp',
|
|
456
|
+
country: '',
|
|
457
|
+
killSwitch: true,
|
|
458
|
+
login: 'none',
|
|
459
|
+
loginSavedAt: null,
|
|
460
|
+
running: null,
|
|
461
|
+
};
|
|
462
|
+
const lines = pageLines(vpnPage(sample)).join('\n');
|
|
463
|
+
t.check(lines.includes('Next: sandbox vpn login') &&
|
|
464
|
+
VPN_WARNINGS.every((w) => lines.includes(w)) &&
|
|
465
|
+
!lines.includes('Running now'), 'vpn status page: the facts, the next step on the command line only, the warnings', lines);
|
|
466
|
+
t.equal(pageActions(vpnPage({ ...sample, provider: 'wireguard-file' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set'], 'vpn: wireguard needs no login and offers only turning the VPN on or off');
|
|
467
|
+
t.equal(pageActions(vpnPage({ ...sample, login: 'saved', loginSavedAt: '2026-10-14T09:30:00.000Z' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set', 'vpn.login', 'vpn.logout'], 'a saved login offers turning the VPN on or off, saving the login again and deleting it');
|
|
468
|
+
t.equal(vpnLoginRowOf(sample).hint, 'not saved', 'the Settings row says not saved when no login is saved');
|
|
469
|
+
const featureStorage = t.dir('vpn-feature');
|
|
470
|
+
const featureCtx = {
|
|
471
|
+
storageRoot: featureStorage,
|
|
472
|
+
dataPath: t.at('vpn-feature', 'data'),
|
|
473
|
+
loaded: { config: { vpn: { ...DEFAULTS.vpn, provider: 'nordvpn' } } },
|
|
474
|
+
};
|
|
475
|
+
const read = (await runDefinition('vpn.status', featureCtx, { tunnel: false }, fakeIO().io));
|
|
476
|
+
t.check(read.login === 'none' && read.running === null, 'vpn status reads the saved login without the tunnel request', JSON.stringify(read));
|
|
477
|
+
const { io: loginIO, seen: loginSeen } = fakeIO();
|
|
478
|
+
await t.throws(() => runDefinition('vpn.login', featureCtx, { password: PASSWORD }, loginIO), 'does not work', 'the login is never accepted as an input');
|
|
479
|
+
t.check(loginSeen.questions.length === 0, 'a login passed on the command line asks nothing');
|
|
480
|
+
// The on-PC guard only refuses inside a sandbox, so it is tested there; elsewhere there is nothing to assert.
|
|
481
|
+
if (insideSandbox()) {
|
|
482
|
+
const { io, seen } = fakeIO();
|
|
483
|
+
await t.throws(() => runDefinition('vpn.login', featureCtx, {}, io), 'inside the sandbox already', 'vpn login refuses inside the sandbox');
|
|
484
|
+
t.check(seen.questions.length === 0, 'and asks no question first');
|
|
485
|
+
}
|
|
486
|
+
const service = new VpnService(featureCtx, SecretStore.memory());
|
|
487
|
+
service.save({ username: USERNAME, password: PASSWORD });
|
|
488
|
+
const onDisk = fs.readFileSync(path.join(featureStorage, 'vpn', 'nordvpn.json'), 'utf8');
|
|
489
|
+
t.check(!onDisk.includes(PASSWORD) && !onDisk.includes(USERNAME), 'the saved file holds no login in clear');
|
|
490
|
+
const report = await service.report({ tunnel: false });
|
|
491
|
+
t.check(report.login === 'saved' &&
|
|
492
|
+
!JSON.stringify(report).includes(PASSWORD) &&
|
|
493
|
+
!JSON.stringify(report).includes(USERNAME), 'a saved login shows as saved, and the status never holds it');
|
|
494
|
+
t.check(service.remove() && service.loginState() === 'none', 'deleting the login leaves none saved');
|
|
411
495
|
},
|
|
412
496
|
};
|
|
@@ -8,7 +8,7 @@ import WebSocket from 'ws';
|
|
|
8
8
|
import { cancelled } from '../app/kernel/index.js';
|
|
9
9
|
import { webSession } from '../cli/services/index.js';
|
|
10
10
|
import { fail } from '../core/util/index.js';
|
|
11
|
-
import { cookieValue, csrfOk, hostAllowed, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
|
|
11
|
+
import { cookieValue, csrfOk, hostAllowed, LiveHub, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
|
|
12
12
|
import { encodeFrame, FrameReader, FrameType } from '../shared/pty/index.js';
|
|
13
13
|
import { CSRF_HEADER, CSRF_VALUE, parseClientMessage } from '../shared/web/index.js';
|
|
14
14
|
const TOKEN = 'a'.repeat(64);
|
|
@@ -153,7 +153,7 @@ export const webSuite = {
|
|
|
153
153
|
t.check(good.ok, 'the right token logs in');
|
|
154
154
|
const cookie = good.ok ? good.cookie : '';
|
|
155
155
|
t.check(auth.signedIn('x=1; sbxweb=' + cookie), 'the cookie signs in');
|
|
156
|
-
t.check(!auth.signedIn('sbxweb=' + cookie.replace(/.$/, '0')) && !auth.signedIn(undefined), 'another cookie does not');
|
|
156
|
+
t.check(!auth.signedIn('sbxweb=' + cookie.replace(/.$/, (c) => (c === '0' ? '1' : '0'))) && !auth.signedIn(undefined), 'another cookie does not');
|
|
157
157
|
clock += 13 * 60 * 60 * 1000;
|
|
158
158
|
t.check(!auth.signedIn('sbxweb=' + cookie), 'a cookie unused for 12 hours expires');
|
|
159
159
|
const limited = new WebAuth(TOKEN, () => clock);
|
|
@@ -210,12 +210,46 @@ export const webSuite = {
|
|
|
210
210
|
t.excludes(shown, ATTACH_TOKEN, 'the attach token is not in what the browser gets');
|
|
211
211
|
t.excludes(shown, '1234', 'nor the port');
|
|
212
212
|
t.excludes(shown, '\x1b', 'text from the sandbox is made safe');
|
|
213
|
+
t.section('live queries are shared, slow down when calm, and stop when nobody watches');
|
|
214
|
+
{
|
|
215
|
+
let value = 1;
|
|
216
|
+
let reads = 0;
|
|
217
|
+
const host = {
|
|
218
|
+
kind: () => 'query',
|
|
219
|
+
liveMs: () => 40,
|
|
220
|
+
run: async () => {
|
|
221
|
+
reads++;
|
|
222
|
+
return { value };
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const hub = new LiveHub(host);
|
|
226
|
+
const seenA = [];
|
|
227
|
+
const seenB = [];
|
|
228
|
+
const stopA = hub.watch('q', {}, (e) => seenA.push(e));
|
|
229
|
+
await t.waitFor(() => seenA.length === 1, 2000);
|
|
230
|
+
const readsBeforeB = reads;
|
|
231
|
+
const stopB = hub.watch('q', {}, (e) => seenB.push(e));
|
|
232
|
+
t.check(reads === readsBeforeB && seenB.length === 1 && hub.size === 1, 'a second watcher of the same query gets the last answer at once, without another read');
|
|
233
|
+
const before = reads;
|
|
234
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
235
|
+
const calmReads = reads - before;
|
|
236
|
+
t.check(calmReads >= 3 && calmReads < 20, 'an unchanged answer is read less often than every interval (' + calmReads + ' reads in 20 intervals)');
|
|
237
|
+
t.check(seenA.length === 1, 'an unchanged answer is not sent again');
|
|
238
|
+
value = 2;
|
|
239
|
+
hub.poke();
|
|
240
|
+
await t.waitFor(() => seenA.length === 2 && seenB.length === 2, 2000);
|
|
241
|
+
t.check(true, 'after an operation (poke) every watcher gets the new answer at once');
|
|
242
|
+
stopA();
|
|
243
|
+
stopB();
|
|
244
|
+
const stopped = reads;
|
|
245
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
246
|
+
t.check(hub.size === 0 && reads === stopped, 'with nobody watching, nothing is read');
|
|
247
|
+
}
|
|
213
248
|
t.section('the server');
|
|
214
249
|
const fake = new FakePtyHost();
|
|
215
250
|
await fake.start();
|
|
216
251
|
const endpoint = { addresses: ['127.0.0.1'], port: fake.port, token: ATTACH_TOKEN };
|
|
217
252
|
const listed = webSession(session('abc123'), 0);
|
|
218
|
-
const created = [];
|
|
219
253
|
const backend = {
|
|
220
254
|
info: () => Promise.resolve({
|
|
221
255
|
sandbox: 'running',
|
|
@@ -227,10 +261,6 @@ export const webSuite = {
|
|
|
227
261
|
}),
|
|
228
262
|
sessions: () => Promise.resolve([listed]),
|
|
229
263
|
endpoint: (id) => (id === 'abc123' ? endpoint : null),
|
|
230
|
-
create: (request) => {
|
|
231
|
-
created.push(request);
|
|
232
|
-
return Promise.resolve(listed);
|
|
233
|
-
},
|
|
234
264
|
};
|
|
235
265
|
let stopped = false;
|
|
236
266
|
const rpc = fakeRpc();
|
|
@@ -241,6 +271,7 @@ export const webSuite = {
|
|
|
241
271
|
staticRoot: root,
|
|
242
272
|
rpc: rpc.host,
|
|
243
273
|
onStop: () => (stopped = true),
|
|
274
|
+
heartbeatMs: 200,
|
|
244
275
|
});
|
|
245
276
|
await server.start();
|
|
246
277
|
const { port } = server;
|
|
@@ -276,10 +307,7 @@ export const webSuite = {
|
|
|
276
307
|
t.check(list.status === 200 && JSON.parse(list.body).length === 1, 'a signed-in browser lists sessions');
|
|
277
308
|
t.excludes(list.body, ATTACH_TOKEN, 'the list has no attach token');
|
|
278
309
|
t.equal((await send(port, 'GET', '/api/info', { Cookie })).status, 200, 'and reads the info');
|
|
279
|
-
t.equal((await send(port, 'POST', '/api/sessions', { ...json, Cookie }, '{}')).status,
|
|
280
|
-
t.equal((await send(port, 'POST', '/api/sessions', { ...ajax, Cookie }, '{"harness":"../x","project":"p"}')).status, 400, 'odd names are refused');
|
|
281
|
-
const made = await send(port, 'POST', '/api/sessions', { ...ajax, Cookie }, '{"harness":"claude","project":"app"}');
|
|
282
|
-
t.check(made.status === 201 && created.length === 1, 'a new session is requested');
|
|
310
|
+
t.equal((await send(port, 'POST', '/api/sessions', { ...json, Cookie }, '{}')).status, 404, 'starting an agent is an operation now: there is no POST /api/sessions');
|
|
283
311
|
t.section('the terminal bridge');
|
|
284
312
|
const url = 'ws://127.0.0.1:' + port + '/api/sessions/abc123/terminal?cols=100&rows=40';
|
|
285
313
|
const attempts = [
|
|
@@ -380,6 +408,25 @@ export const webSuite = {
|
|
|
380
408
|
leaver.socket.close();
|
|
381
409
|
await t.waitFor(() => leaver.closeCode !== null, 5000);
|
|
382
410
|
t.check(rpc.ran.length === runs + 1, 'a browser that leaves mid-question answers no (the call ends)');
|
|
411
|
+
t.section('connections never pile up');
|
|
412
|
+
const tabs = [];
|
|
413
|
+
for (let i = 0; i < 9; i++) {
|
|
414
|
+
const tab = new Tab(rpcUrl, { Origin: origin, Cookie });
|
|
415
|
+
tabs.push(tab);
|
|
416
|
+
await t.waitFor(() => tab.opened || tab.failed, 5000);
|
|
417
|
+
}
|
|
418
|
+
await t.waitFor(() => server.openSockets('rpc') <= 8, 5000);
|
|
419
|
+
t.check(tabs.every((tab) => tab.opened) && server.openSockets('rpc') === 8, 'a new page always connects: at the limit the oldest connection makes room', 'open: ' + server.openSockets('rpc'));
|
|
420
|
+
t.check([browser, ...tabs].some((tab) => tab.closeCode === 4000), 'the oldest is closed with "replaced by a newer page"');
|
|
421
|
+
for (const tab of tabs)
|
|
422
|
+
tab.socket.close();
|
|
423
|
+
const silent = new WebSocket(rpcUrl, { headers: { Origin: origin, Cookie }, autoPong: false });
|
|
424
|
+
let silentClosed = false;
|
|
425
|
+
silent.on('close', () => (silentClosed = true));
|
|
426
|
+
silent.on('error', () => undefined);
|
|
427
|
+
await t.waitFor(() => silent.readyState === WebSocket.OPEN, 5000);
|
|
428
|
+
await t.waitFor(() => silentClosed, 3000);
|
|
429
|
+
t.check(silentClosed, 'a page that stops answering pings (gone without closing) is dropped');
|
|
383
430
|
t.section('stopping');
|
|
384
431
|
t.equal((await send(port, 'POST', '/api/stop', { ...json, Cookie }, '{}')).status, 403, 'stop needs CSRF and Origin too');
|
|
385
432
|
t.equal((await send(port, 'POST', '/api/stop', { ...ajax, Cookie }, '{}')).status, 200, 'a signed-in browser can stop it');
|
|
@@ -9,6 +9,8 @@ export declare class RunDir {
|
|
|
9
9
|
readonly vpnFile: string;
|
|
10
10
|
/** What the memory governor in the sandbox is doing (pressure.json). */
|
|
11
11
|
readonly pressureFile: string;
|
|
12
|
+
/** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
|
|
13
|
+
readonly budgetFile: string;
|
|
12
14
|
readonly requestsDir: string;
|
|
13
15
|
readonly responsesDir: string;
|
|
14
16
|
readonly sessionsDir: string;
|
|
@@ -36,6 +36,8 @@ export class RunDir {
|
|
|
36
36
|
vpnFile;
|
|
37
37
|
/** What the memory governor in the sandbox is doing (pressure.json). */
|
|
38
38
|
pressureFile;
|
|
39
|
+
/** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
|
|
40
|
+
budgetFile;
|
|
39
41
|
requestsDir;
|
|
40
42
|
responsesDir;
|
|
41
43
|
sessionsDir;
|
|
@@ -49,6 +51,7 @@ export class RunDir {
|
|
|
49
51
|
this.sandboxFile = path.join(this.root, 'sandbox.json');
|
|
50
52
|
this.vpnFile = path.join(this.root, 'vpn.json');
|
|
51
53
|
this.pressureFile = path.join(this.root, 'pressure.json');
|
|
54
|
+
this.budgetFile = path.join(this.root, 'budget.json');
|
|
52
55
|
this.requestsDir = path.join(this.root, 'requests');
|
|
53
56
|
this.responsesDir = path.join(this.root, 'responses');
|
|
54
57
|
this.sessionsDir = path.join(this.root, 'sessions');
|
|
@@ -556,6 +556,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
556
556
|
id: z.ZodString;
|
|
557
557
|
reason: z.ZodString;
|
|
558
558
|
seconds: z.ZodNumber;
|
|
559
|
+
level: z.ZodOptional<z.ZodEnum<{
|
|
560
|
+
slow: "slow";
|
|
561
|
+
severe: "severe";
|
|
562
|
+
}>>;
|
|
559
563
|
createdAt: z.ZodString;
|
|
560
564
|
}, z.core.$strict>], "kind">;
|
|
561
565
|
/** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
|
|
@@ -565,6 +569,7 @@ export declare const PressureStatusSchema: z.ZodObject<{
|
|
|
565
569
|
throttled: "throttled";
|
|
566
570
|
suspending: "suspending";
|
|
567
571
|
}>;
|
|
572
|
+
step: z.ZodOptional<z.ZodNumber>;
|
|
568
573
|
profile: z.ZodEnum<{
|
|
569
574
|
normal: "normal";
|
|
570
575
|
gentle: "gentle";
|
|
@@ -577,6 +582,7 @@ export declare const PressureStatusSchema: z.ZodObject<{
|
|
|
577
582
|
pid: z.ZodNumber;
|
|
578
583
|
seconds: z.ZodNumber;
|
|
579
584
|
}, z.core.$strip>>;
|
|
585
|
+
cpuPercent: z.ZodOptional<z.ZodNumber>;
|
|
580
586
|
pcLow: z.ZodBoolean;
|
|
581
587
|
lastAction: z.ZodString;
|
|
582
588
|
updatedAt: z.ZodString;
|
|
@@ -645,6 +651,7 @@ export declare const VpnStatusSchema: z.ZodObject<{
|
|
|
645
651
|
}>;
|
|
646
652
|
message: z.ZodString;
|
|
647
653
|
killSwitch: z.ZodBoolean;
|
|
654
|
+
killSwitchHeld: z.ZodOptional<z.ZodBoolean>;
|
|
648
655
|
server: z.ZodOptional<z.ZodString>;
|
|
649
656
|
publicIp: z.ZodOptional<z.ZodString>;
|
|
650
657
|
updatedAt: z.ZodString;
|
|
@@ -100,20 +100,26 @@ export const GuestRequestSchema = z.discriminatedUnion('kind', [
|
|
|
100
100
|
id: IdSchema,
|
|
101
101
|
reason: Text(300),
|
|
102
102
|
seconds: z.number().int().min(10).max(300),
|
|
103
|
+
/** "severe": the PC is nearly out of memory; builds go to the governor's last step at once. */
|
|
104
|
+
level: z.enum(['slow', 'severe']).optional(),
|
|
103
105
|
createdAt: TimeSchema,
|
|
104
106
|
}),
|
|
105
107
|
]);
|
|
106
108
|
/** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
|
|
107
109
|
export const PressureStatusSchema = z.object({
|
|
108
110
|
level: z.enum(['ok', 'throttled', 'suspending']),
|
|
111
|
+
/** The governor's dial, 0 (nothing wrong) to 4 (one compiler at a time). */
|
|
112
|
+
step: z.number().int().min(0).max(4).optional(),
|
|
109
113
|
profile: z.enum(['normal', 'gentle']),
|
|
110
114
|
/** Share of the sandbox's RAM still free and of its commit limit in use (percent). */
|
|
111
115
|
ramFreePercent: z.number().int().min(0).max(100),
|
|
112
116
|
commitPercent: z.number().int().min(0).max(100),
|
|
113
117
|
/** Programs running at lower priority because of it. */
|
|
114
118
|
lowered: z.number().int().min(0).max(1024),
|
|
115
|
-
/**
|
|
119
|
+
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
116
120
|
paused: z.array(z.object({ name: Text(64), pid: z.number().int(), seconds: z.number().int().min(0).max(60) })).max(8),
|
|
121
|
+
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
122
|
+
cpuPercent: z.number().int().min(0).max(100).optional(),
|
|
117
123
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
118
124
|
pcLow: z.boolean(),
|
|
119
125
|
lastAction: Text(300),
|
|
@@ -163,6 +169,8 @@ export const VpnStatusSchema = z.object({
|
|
|
163
169
|
state: z.enum(['off', 'starting', 'up', 'failed']),
|
|
164
170
|
message: Text(2000),
|
|
165
171
|
killSwitch: z.boolean(),
|
|
172
|
+
/** A direct connection was tried after the kill switch was applied: false = it still got out. */
|
|
173
|
+
killSwitchHeld: z.boolean().optional(),
|
|
166
174
|
/** The server's name, e.g. uk2201.nordvpn.com. */
|
|
167
175
|
server: Text(128).optional(),
|
|
168
176
|
/** What the sandbox's traffic looks like from outside, once the tunnel is up. */
|
|
@@ -9,6 +9,7 @@ export declare const SecretsFileSchema: z.ZodObject<{
|
|
|
9
9
|
password: z.ZodString;
|
|
10
10
|
}, z.core.$strict>>;
|
|
11
11
|
wireguardConfig: z.ZodOptional<z.ZodString>;
|
|
12
|
+
githubToken: z.ZodOptional<z.ZodString>;
|
|
12
13
|
}, z.core.$strict>;
|
|
13
14
|
export type SecretsFile = z.output<typeof SecretsFileSchema>;
|
|
14
15
|
interface WithHarnesses {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// sandbox may change run\, never this folder. The agent in the sandbox can still read what it was given (its own
|
|
5
5
|
// environment, its login): that is inherent. Every part is optional.
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { GithubTokenSchema } from '../github/index.js';
|
|
7
8
|
const Environment = z.record(z.string().max(256), z.string().max(65536));
|
|
8
9
|
export const SecretsFileSchema = z.strictObject({
|
|
9
10
|
/** The agent the sandbox booted with: its harness environment. */
|
|
@@ -25,6 +26,8 @@ export const SecretsFileSchema = z.strictObject({
|
|
|
25
26
|
.min(1)
|
|
26
27
|
.max(32 * 1024)
|
|
27
28
|
.optional(),
|
|
29
|
+
/** The GitHub token you saved for sandboxes (github.shareToken), only with git access "full". */
|
|
30
|
+
githubToken: GithubTokenSchema.optional(),
|
|
28
31
|
});
|
|
29
32
|
/** The environment a harness gets: its own, then the values that travelled in the secrets file. */
|
|
30
33
|
const withEnvironment = (harness, environment) => (environment ? { ...harness, environment: { ...harness.environment, ...environment } } : harness);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GITHUB_TOKEN_KINDS, GithubTokenSchema, ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenKind, githubTokenProblem, type GhCommand, type GithubTokenKind, } from './token.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const GITHUB_TOKEN_KINDS: readonly ["fine-grained", "classic"];
|
|
3
|
+
export type GithubTokenKind = (typeof GITHUB_TOKEN_KINDS)[number];
|
|
4
|
+
/** The token in the secrets file: the same limits as on the PC. */
|
|
5
|
+
export declare const GithubTokenSchema: z.ZodString;
|
|
6
|
+
/** What kind of personal access token this is, from its prefix (null: not one). */
|
|
7
|
+
export declare function githubTokenKind(token: string): GithubTokenKind | null;
|
|
8
|
+
/** Why this text cannot be saved as the sandbox token (fixed texts: never the token), or null when it can. */
|
|
9
|
+
export declare function githubTokenProblem(token: string): {
|
|
10
|
+
message: string;
|
|
11
|
+
fix: string;
|
|
12
|
+
} | null;
|
|
13
|
+
/** One gh run: the program, its arguments, what goes on its stdin, and its environment. */
|
|
14
|
+
export interface GhCommand {
|
|
15
|
+
readonly file: string;
|
|
16
|
+
readonly args: readonly string[];
|
|
17
|
+
readonly input?: string;
|
|
18
|
+
readonly env: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `gh auth login` with the token on stdin, never in the arguments (other processes can read those). The login is
|
|
22
|
+
* kept as a file in configDir (--insecure-storage, rather than Credential Manager), which is on the sandbox's own disk
|
|
23
|
+
* and gone with it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ghLoginCommand(ghExe: string, configDir: string, token: string, base: Readonly<Record<string, string | undefined>>): GhCommand;
|
|
26
|
+
/** `gh auth setup-git`: https git to github.com asks gh for the login. */
|
|
27
|
+
export declare function ghSetupGitCommand(ghExe: string, configDir: string, base: Readonly<Record<string, string | undefined>>): GhCommand;
|
|
28
|
+
/** Text from gh, safe to log: the token blanked out should it ever appear, and kept short. */
|
|
29
|
+
export declare function ghOutputText(text: string, token: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// A GitHub token for sandboxes (github.shareToken): the rules for one (both sides check them) and the gh commands the
|
|
2
|
+
// sandbox runs with it. Pure. The token is never part of a message, an argument or a log line: gh reads it on stdin.
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const GITHUB_TOKEN_KINDS = ['fine-grained', 'classic'];
|
|
5
|
+
const MIN_LENGTH = 20;
|
|
6
|
+
const MAX_LENGTH = 255;
|
|
7
|
+
/** Printable ASCII without spaces: what GitHub's tokens are made of. */
|
|
8
|
+
const PRINTABLE = /^[\x21-\x7e]+$/;
|
|
9
|
+
/** The token in the secrets file: the same limits as on the PC. */
|
|
10
|
+
export const GithubTokenSchema = z.string().min(MIN_LENGTH).max(MAX_LENGTH).regex(PRINTABLE);
|
|
11
|
+
/** Prefixes of tokens that are not personal access tokens (OAuth from `gh auth login`, app and refresh tokens). */
|
|
12
|
+
const OTHER_TOKENS = {
|
|
13
|
+
gho_: 'This is an OAuth token (from gh auth login or gh auth token), which can reach everything your account can.',
|
|
14
|
+
ghu_: 'This is a GitHub App user token, not a personal access token.',
|
|
15
|
+
ghs_: 'This is a GitHub App installation token, not a personal access token.',
|
|
16
|
+
ghr_: 'This is a refresh token, not a personal access token.',
|
|
17
|
+
};
|
|
18
|
+
const CREATE_FINE_GRAINED = 'Create a fine-grained token at https://github.com/settings/personal-access-tokens/new with only the repositories and permissions the agent needs, then run: sandbox github login';
|
|
19
|
+
/** What kind of personal access token this is, from its prefix (null: not one). */
|
|
20
|
+
export function githubTokenKind(token) {
|
|
21
|
+
if (token.startsWith('github_pat_'))
|
|
22
|
+
return 'fine-grained';
|
|
23
|
+
if (token.startsWith('ghp_'))
|
|
24
|
+
return 'classic';
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** Why this text cannot be saved as the sandbox token (fixed texts: never the token), or null when it can. */
|
|
28
|
+
export function githubTokenProblem(token) {
|
|
29
|
+
if (!token)
|
|
30
|
+
return { message: 'No token was typed.', fix: CREATE_FINE_GRAINED };
|
|
31
|
+
if (!PRINTABLE.test(token))
|
|
32
|
+
return {
|
|
33
|
+
message: 'The token may not contain spaces, line breaks or other special characters.',
|
|
34
|
+
fix: 'Copy the token again from GitHub (only the token itself) and run: sandbox github login',
|
|
35
|
+
};
|
|
36
|
+
if (token.length < MIN_LENGTH || token.length > MAX_LENGTH)
|
|
37
|
+
return {
|
|
38
|
+
message: 'That is not the length of a GitHub token.',
|
|
39
|
+
fix: 'Copy the whole token again from GitHub and run: sandbox github login',
|
|
40
|
+
};
|
|
41
|
+
const other = Object.keys(OTHER_TOKENS).find((p) => token.startsWith(p));
|
|
42
|
+
if (other)
|
|
43
|
+
return { message: OTHER_TOKENS[other], fix: CREATE_FINE_GRAINED };
|
|
44
|
+
if (!githubTokenKind(token))
|
|
45
|
+
return {
|
|
46
|
+
message: 'That is not a GitHub personal access token (they start with github_pat_ or ghp_).',
|
|
47
|
+
fix: CREATE_FINE_GRAINED,
|
|
48
|
+
};
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** Variables that would make gh use another token than the one it is given (gh refuses --with-token with them). */
|
|
52
|
+
const TOKEN_VARIABLES = /^(GH_TOKEN|GITHUB_TOKEN|GH_ENTERPRISE_TOKEN|GITHUB_ENTERPRISE_TOKEN)$/i;
|
|
53
|
+
const ghEnv = (base, configDir) => ({
|
|
54
|
+
...Object.fromEntries(Object.entries(base).filter((e) => e[1] !== undefined && !TOKEN_VARIABLES.test(e[0]))),
|
|
55
|
+
GH_CONFIG_DIR: configDir,
|
|
56
|
+
GH_PROMPT_DISABLED: '1',
|
|
57
|
+
GH_NO_UPDATE_NOTIFIER: '1',
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* `gh auth login` with the token on stdin, never in the arguments (other processes can read those). The login is
|
|
61
|
+
* kept as a file in configDir (--insecure-storage, rather than Credential Manager), which is on the sandbox's own disk
|
|
62
|
+
* and gone with it.
|
|
63
|
+
*/
|
|
64
|
+
export function ghLoginCommand(ghExe, configDir, token, base) {
|
|
65
|
+
return {
|
|
66
|
+
file: ghExe,
|
|
67
|
+
args: [
|
|
68
|
+
'auth',
|
|
69
|
+
'login',
|
|
70
|
+
'--hostname',
|
|
71
|
+
'github.com',
|
|
72
|
+
'--git-protocol',
|
|
73
|
+
'https',
|
|
74
|
+
'--insecure-storage',
|
|
75
|
+
'--with-token',
|
|
76
|
+
],
|
|
77
|
+
input: token + '\n',
|
|
78
|
+
env: ghEnv(base, configDir),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** `gh auth setup-git`: https git to github.com asks gh for the login. */
|
|
82
|
+
export function ghSetupGitCommand(ghExe, configDir, base) {
|
|
83
|
+
return { file: ghExe, args: ['auth', 'setup-git', '--hostname', 'github.com'], env: ghEnv(base, configDir) };
|
|
84
|
+
}
|
|
85
|
+
/** Text from gh, safe to log: the token blanked out should it ever appear, and kept short. */
|
|
86
|
+
export function ghOutputText(text, token) {
|
|
87
|
+
const clean = token ? text.split(token).join('***') : text;
|
|
88
|
+
return clean.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CopyTreeResult {
|
|
2
|
+
readonly copied: number;
|
|
3
|
+
/** Files (or folders) that were gone by the time they were reached. */
|
|
4
|
+
readonly vanished: number;
|
|
5
|
+
}
|
|
6
|
+
/** Copies `from` into `to`; `skip(name)` leaves out files by name. Any error except "no such file" is thrown. */
|
|
7
|
+
export declare function copyTreeTolerant(from: string, to: string, skip?: (name: string) => boolean): CopyTreeResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copies a folder that something else is still writing to: a file that disappears between the listing and the copy
|
|
2
|
+
// (a spool file the agent's hook just consumed, a SQLite journal that was committed) is skipped, not an error.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const isGone = (e) => e?.code === 'ENOENT';
|
|
6
|
+
/** Copies `from` into `to`; `skip(name)` leaves out files by name. Any error except "no such file" is thrown. */
|
|
7
|
+
export function copyTreeTolerant(from, to, skip = () => false) {
|
|
8
|
+
let copied = 0;
|
|
9
|
+
let vanished = 0;
|
|
10
|
+
const walk = (src, dest) => {
|
|
11
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = fs.readdirSync(src, { withFileTypes: true });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (!isGone(e))
|
|
18
|
+
throw e;
|
|
19
|
+
vanished++;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (skip(entry.name))
|
|
24
|
+
continue;
|
|
25
|
+
const source = path.join(src, entry.name);
|
|
26
|
+
const target = path.join(dest, entry.name);
|
|
27
|
+
try {
|
|
28
|
+
if (entry.isDirectory())
|
|
29
|
+
walk(source, target);
|
|
30
|
+
else if (entry.isFile()) {
|
|
31
|
+
fs.copyFileSync(source, target);
|
|
32
|
+
copied++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (!isGone(e))
|
|
37
|
+
throw e;
|
|
38
|
+
vanished++;
|
|
39
|
+
fs.rmSync(target, { force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(from, to);
|
|
44
|
+
return { copied, vanished };
|
|
45
|
+
}
|