windows-development-sandbox 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.example.json +12 -1
- package/dist/app/context.d.ts +8 -0
- package/dist/app/context.js +12 -0
- package/dist/app/features/defs.d.ts +322 -0
- package/dist/app/features/defs.js +28 -0
- package/dist/app/features/sandbox/index.d.ts +2 -0
- package/dist/app/features/sandbox/index.js +3 -0
- package/dist/app/features/sandbox/sandbox.def.d.ts +193 -0
- package/dist/app/features/sandbox/sandbox.def.js +245 -0
- package/dist/app/features/sandbox/sandbox.run.d.ts +72 -0
- package/dist/app/features/sandbox/sandbox.run.js +98 -0
- package/dist/app/features/sessions/index.d.ts +2 -0
- package/dist/app/features/sessions/index.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +141 -0
- package/dist/app/features/sessions/sessions.def.js +176 -0
- package/dist/app/features/sessions/sessions.run.d.ts +39 -0
- package/dist/app/features/sessions/sessions.run.js +58 -0
- package/dist/{cli/menu/settings/categories.d.ts → app/features/settings/categories.def.d.ts} +2 -0
- package/dist/{cli/menu/settings/categories.js → app/features/settings/categories.def.js} +24 -5
- package/dist/app/features/settings/index.d.ts +5 -0
- package/dist/app/features/settings/index.js +6 -0
- package/dist/{cli/menu/settings/config-file.d.ts → app/features/settings/settings-file.d.ts} +1 -1
- package/dist/{cli/menu/settings/config-file.js → app/features/settings/settings-file.js} +1 -1
- package/dist/app/features/settings/settings-rows.d.ts +35 -0
- package/dist/app/features/settings/settings-rows.js +94 -0
- package/dist/app/features/settings/settings.def.d.ts +157 -0
- package/dist/app/features/settings/settings.def.js +179 -0
- package/dist/app/features/settings/settings.run.d.ts +67 -0
- package/dist/app/features/settings/settings.run.js +84 -0
- package/dist/app/kernel/define.d.ts +143 -0
- package/dist/app/kernel/define.js +42 -0
- package/dist/app/kernel/errors.d.ts +34 -0
- package/dist/app/kernel/errors.js +45 -0
- package/dist/app/kernel/fields.d.ts +70 -0
- package/dist/app/kernel/fields.js +243 -0
- package/dist/app/kernel/index.d.ts +8 -0
- package/dist/app/kernel/index.js +10 -0
- package/dist/app/kernel/io.d.ts +193 -0
- package/dist/app/kernel/io.js +126 -0
- package/dist/app/kernel/rpc.d.ts +156 -0
- package/dist/app/kernel/rpc.js +47 -0
- package/dist/app/kernel/text.d.ts +6 -0
- package/dist/app/kernel/text.js +57 -0
- package/dist/app/kernel/view.d.ts +146 -0
- package/dist/app/kernel/view.js +28 -0
- package/dist/app/registry.d.ts +10 -0
- package/dist/app/registry.js +50 -0
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.js +6 -0
- package/dist/cli/commands/engine.d.ts +2 -0
- package/dist/cli/commands/engine.js +60 -0
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/launch.js +49 -7
- package/dist/cli/commands/launch/prepare.js +3 -3
- package/dist/cli/commands/launch/running.js +2 -1
- package/dist/cli/commands/launch/summary.d.ts +2 -0
- package/dist/cli/commands/maintenance/config-share.d.ts +12 -0
- package/dist/cli/commands/maintenance/config-share.js +80 -0
- package/dist/cli/commands/maintenance/config.js +8 -82
- package/dist/cli/commands/maintenance/logs.js +14 -0
- package/dist/cli/commands/registry.js +3 -7
- package/dist/cli/commands/repair/checks/contained.d.ts +2 -0
- package/dist/cli/commands/repair/checks/contained.js +139 -0
- package/dist/cli/commands/repair/checks/hcs.d.ts +7 -0
- package/dist/cli/commands/repair/checks/hcs.js +28 -0
- package/dist/cli/commands/repair/checks/index.js +4 -0
- package/dist/cli/commands/repair/checks/pc.js +32 -1
- package/dist/cli/commands/repair/checks/sandbox.js +16 -1
- package/dist/cli/commands/repair/checks/vpn.js +34 -9
- package/dist/cli/commands/repair/env.js +2 -1
- package/dist/cli/commands/sessions/index.d.ts +0 -4
- package/dist/cli/commands/sessions/index.js +3 -7
- package/dist/cli/commands/shared-use.d.ts +5 -2
- package/dist/cli/commands/shared-use.js +11 -8
- package/dist/cli/commands/storage/contained.d.ts +3 -0
- package/dist/cli/commands/storage/contained.js +179 -0
- package/dist/cli/commands/storage/offer.js +7 -1
- package/dist/cli/commands/storage/storage.js +10 -2
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/commands/vpn.js +20 -3
- package/dist/cli/commands/web.d.ts +5 -0
- package/dist/cli/commands/web.js +40 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/manifest.d.ts +72 -104
- package/dist/cli/manifest.js +64 -39
- package/dist/cli/menu/add-agent.d.ts +1 -1
- package/dist/cli/menu/add-agent.js +1 -1
- package/dist/cli/menu/apps.d.ts +7 -1
- package/dist/cli/menu/apps.js +36 -0
- package/dist/cli/menu/command-fields.d.ts +1 -1
- package/dist/cli/menu/command-form.js +2 -2
- package/dist/cli/menu/io-menu.d.ts +29 -0
- package/dist/cli/menu/io-menu.js +126 -0
- package/dist/cli/menu/live.d.ts +27 -0
- package/dist/cli/menu/live.js +69 -0
- package/dist/cli/menu/memory.d.ts +19 -0
- package/dist/cli/menu/memory.js +53 -0
- package/dist/cli/menu/menu.d.ts +10 -1
- package/dist/cli/menu/menu.js +27 -9
- package/dist/cli/menu/pages/agents-page.d.ts +3 -1
- package/dist/cli/menu/pages/agents-page.js +12 -5
- package/dist/cli/menu/pages/apps-page.d.ts +3 -1
- package/dist/cli/menu/pages/apps-page.js +19 -2
- package/dist/cli/menu/pages/list-page.d.ts +27 -0
- package/dist/cli/menu/pages/list-page.js +109 -7
- package/dist/cli/menu/pages/maintenance-page.d.ts +0 -5
- package/dist/cli/menu/pages/maintenance-page.js +17 -19
- package/dist/cli/menu/pages/sandbox-page.d.ts +4 -6
- package/dist/cli/menu/pages/sandbox-page.js +26 -118
- package/dist/cli/menu/pages/scopes.d.ts +1 -1
- package/dist/cli/menu/pages/sessions-page.d.ts +5 -11
- package/dist/cli/menu/pages/sessions-page.js +19 -88
- package/dist/cli/menu/pages/settings-page.d.ts +2 -10
- package/dist/cli/menu/pages/settings-page.js +64 -94
- package/dist/cli/menu/render-view.d.ts +28 -0
- package/dist/cli/menu/render-view.js +112 -0
- package/dist/cli/menu/settings/{path-kind.d.ts → ask-path.d.ts} +1 -7
- package/dist/cli/menu/settings/ask-path.js +14 -0
- package/dist/cli/menu/settings/collections/collection-editor.d.ts +1 -1
- package/dist/cli/menu/settings/collections/string-list-editor.d.ts +1 -2
- package/dist/cli/menu/settings/collections/string-list-editor.js +1 -1
- package/dist/cli/menu/settings/draft.d.ts +1 -1
- package/dist/cli/menu/settings/format.d.ts +1 -3
- package/dist/cli/menu/settings/format.js +6 -36
- package/dist/cli/menu/settings/index.d.ts +1 -3
- package/dist/cli/menu/settings/index.js +3 -3
- package/dist/cli/menu/settings/object-editor.d.ts +1 -1
- package/dist/cli/menu/settings/share.d.ts +36 -0
- package/dist/cli/menu/settings/share.js +131 -0
- package/dist/cli/menu/settings/value-editor.d.ts +5 -1
- package/dist/cli/menu/settings/value-editor.js +57 -54
- package/dist/cli/menu/several.js +1 -1
- package/dist/cli/menu/share-settings.d.ts +21 -0
- package/dist/cli/menu/share-settings.js +112 -0
- package/dist/cli/menu/shell.d.ts +21 -5
- package/dist/cli/menu/shell.js +31 -6
- package/dist/cli/menu/views.d.ts +7 -0
- package/dist/cli/menu/views.js +11 -0
- package/dist/cli/menu/vpn-login.js +8 -0
- package/dist/cli/operations/handler.d.ts +6 -0
- package/dist/cli/operations/handler.js +61 -0
- package/dist/cli/operations/index.d.ts +4 -0
- package/dist/cli/operations/index.js +5 -0
- package/dist/cli/operations/input.d.ts +6 -0
- package/dist/cli/operations/input.js +37 -0
- package/dist/cli/operations/io-cli.d.ts +7 -0
- package/dist/cli/operations/io-cli.js +62 -0
- package/dist/cli/operations/spec.d.ts +18 -0
- package/dist/cli/operations/spec.js +108 -0
- package/dist/cli/output/colors.d.ts +2 -2
- package/dist/cli/output/colors.js +2 -11
- package/dist/cli/output/index.d.ts +1 -0
- package/dist/cli/output/index.js +1 -0
- package/dist/cli/output/print.d.ts +3 -1
- package/dist/cli/output/print.js +7 -3
- package/dist/cli/output/progress.d.ts +15 -0
- package/dist/cli/output/progress.js +44 -0
- package/dist/cli/output/render-view.d.ts +4 -0
- package/dist/cli/output/render-view.js +63 -0
- package/dist/cli/services/agent-commands-service.d.ts +2 -0
- package/dist/cli/services/agent-commands-service.js +11 -2
- package/dist/cli/services/logs-service.js +4 -3
- package/dist/cli/tui/app.d.ts +6 -1
- package/dist/cli/tui/app.js +26 -4
- package/dist/cli/tui/list/list-screen.d.ts +14 -0
- package/dist/cli/tui/list/list-screen.js +64 -36
- package/dist/cli/tui/list/list-types.d.ts +2 -0
- package/dist/cli/tui/terminal.d.ts +7 -0
- package/dist/cli/tui/terminal.js +30 -10
- package/dist/core/config/defaults.js +9 -1
- package/dist/core/schema/config.d.ts +14 -0
- package/dist/core/schema/config.js +16 -1
- package/dist/core/schema/harness.d.ts +1 -0
- package/dist/core/schema/harness.js +5 -0
- package/dist/core/schema/sandbox.d.ts +6 -0
- package/dist/core/schema/sandbox.js +20 -3
- package/dist/core/schema/vpn.d.ts +2 -1
- package/dist/core/schema/vpn.js +4 -4
- package/dist/core/schema/web.d.ts +1 -0
- package/dist/core/schema/web.js +4 -1
- package/dist/core/types.d.ts +23 -5
- package/dist/core/util/fs.d.ts +2 -0
- package/dist/core/util/fs.js +9 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/agents/agent-files.d.ts +5 -0
- package/dist/guest/agents/agent-files.js +4 -0
- package/dist/guest/agents/local-state.d.ts +2 -0
- package/dist/guest/agents/local-state.js +21 -1
- package/dist/guest/boot/boot.js +5 -2
- package/dist/guest/daemon/daemon.js +4 -0
- package/dist/guest/daemon/sessions.d.ts +5 -0
- package/dist/guest/daemon/sessions.js +17 -3
- package/dist/guest/entries/daemon.js +3 -0
- package/dist/guest/governor/decide.d.ts +71 -0
- package/dist/guest/governor/decide.js +211 -0
- package/dist/guest/governor/governor.d.ts +45 -0
- package/dist/guest/governor/governor.js +230 -0
- package/dist/guest/governor/index.d.ts +2 -0
- package/dist/guest/governor/index.js +2 -0
- package/dist/guest/network/firewall.d.ts +1 -0
- package/dist/guest/network/firewall.js +65 -23
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/process.js +53 -5
- package/dist/guest/sessions/viewer.js +3 -1
- package/dist/guest/setup/claude-mem.js +12 -4
- package/dist/guest/toolchains/languages/node.js +29 -3
- package/dist/guest/toolchains/native/rust-extras.js +4 -2
- package/dist/guest/vpn/vpn.js +69 -21
- package/dist/guest/vpn/wireguard.d.ts +32 -0
- package/dist/guest/vpn/wireguard.js +132 -0
- package/dist/host/agent-commands/commands.d.ts +4 -0
- package/dist/host/agent-commands/commands.js +31 -7
- package/dist/host/audit/log.d.ts +1 -1
- package/dist/host/channel/attach.d.ts +3 -1
- package/dist/host/channel/attach.js +5 -2
- package/dist/host/channel/channel.d.ts +2 -0
- package/dist/host/channel/channel.js +4 -0
- package/dist/host/engines/engine.d.ts +12 -0
- package/dist/host/engines/engine.js +1 -0
- package/dist/host/engines/hcs/check.d.ts +25 -0
- package/dist/host/engines/hcs/check.js +95 -0
- package/dist/host/engines/hcs/documents.d.ts +44 -0
- package/dist/host/engines/hcs/documents.js +155 -0
- package/dist/host/engines/hcs/engine.d.ts +39 -0
- package/dist/host/engines/hcs/engine.js +171 -0
- package/dist/host/engines/hcs/helper.d.ts +43 -0
- package/dist/host/engines/hcs/helper.js +173 -0
- package/dist/host/engines/hcs/images.d.ts +37 -0
- package/dist/host/engines/hcs/images.js +152 -0
- package/dist/host/engines/index.d.ts +7 -0
- package/dist/host/engines/index.js +5 -0
- package/dist/host/engines/registry.d.ts +16 -0
- package/dist/host/engines/registry.js +30 -0
- package/dist/host/engines/types.d.ts +58 -0
- package/dist/host/engines/types.js +1 -0
- package/dist/host/engines/windows-sandbox/engine.d.ts +16 -0
- package/dist/host/engines/windows-sandbox/engine.js +63 -0
- package/dist/host/engines/windows-sandbox/wsb-text.d.ts +2 -0
- package/dist/host/engines/windows-sandbox/wsb-text.js +29 -0
- package/dist/host/launch/generate.d.ts +5 -1
- package/dist/host/launch/generate.js +50 -8
- package/dist/host/launch/index.d.ts +3 -1
- package/dist/host/launch/index.js +3 -1
- package/dist/host/launch/launch-plan.d.ts +13 -0
- package/dist/host/launch/launch-plan.js +26 -0
- package/dist/host/launch/mappings.js +2 -1
- package/dist/host/launch/risk-notes.d.ts +10 -0
- package/dist/host/launch/risk-notes.js +18 -0
- package/dist/host/launch/session-file.d.ts +2 -0
- package/dist/host/launch/session-file.js +7 -1
- package/dist/host/launch/wsb-file.d.ts +28 -4
- package/dist/host/launch/wsb-file.js +61 -42
- package/dist/host/life/crash-report.d.ts +27 -1
- package/dist/host/life/crash-report.js +173 -7
- package/dist/host/life/host-memory.d.ts +41 -0
- package/dist/host/life/host-memory.js +140 -0
- package/dist/host/life/index.d.ts +2 -0
- package/dist/host/life/index.js +2 -0
- package/dist/host/life/stop-events.d.ts +2 -0
- package/dist/host/life/stop-events.js +20 -3
- package/dist/host/life/stop-warning.js +1 -1
- package/dist/host/life/watchdog.d.ts +56 -0
- package/dist/host/life/watchdog.js +124 -0
- package/dist/host/native.d.ts +3 -2
- package/dist/host/native.js +22 -3
- package/dist/host/profile/claude-login.js +3 -2
- package/dist/host/sandbox/contained-deps.d.ts +2 -0
- package/dist/host/sandbox/contained-deps.js +17 -0
- package/dist/host/sandbox/index.d.ts +2 -0
- package/dist/host/sandbox/index.js +2 -0
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +7 -7
- package/dist/host/sandbox/tasklist.d.ts +13 -0
- package/dist/host/sandbox/tasklist.js +47 -0
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/current.js +2 -6
- package/dist/host/storage/contained/check.d.ts +9 -0
- package/dist/host/storage/contained/check.js +37 -0
- package/dist/host/storage/contained/contained-storage.d.ts +66 -0
- package/dist/host/storage/contained/contained-storage.js +168 -0
- package/dist/host/storage/contained/disk-port.d.ts +43 -0
- package/dist/host/storage/contained/disk-port.js +122 -0
- package/dist/host/storage/contained/index.d.ts +9 -0
- package/dist/host/storage/contained/index.js +8 -0
- package/dist/host/storage/contained/layout.d.ts +28 -0
- package/dist/host/storage/contained/layout.js +45 -0
- package/dist/host/storage/contained/resolve.d.ts +4 -0
- package/dist/host/storage/contained/resolve.js +30 -0
- package/dist/host/storage/contained/setting.d.ts +1 -0
- package/dist/host/storage/contained/setting.js +5 -0
- package/dist/host/storage/contained/state.d.ts +89 -0
- package/dist/host/storage/contained/state.js +166 -0
- package/dist/host/storage/contained/transfer.d.ts +24 -0
- package/dist/host/storage/contained/transfer.js +121 -0
- package/dist/host/storage/index.d.ts +2 -0
- package/dist/host/storage/index.js +2 -0
- package/dist/host/storage/providers/contained.d.ts +15 -0
- package/dist/host/storage/providers/contained.js +39 -0
- package/dist/host/storage/providers/folder.d.ts +12 -0
- package/dist/host/storage/providers/folder.js +20 -0
- package/dist/host/storage/providers/index.d.ts +4 -0
- package/dist/host/storage/providers/index.js +3 -0
- package/dist/host/storage/providers/registry.d.ts +9 -0
- package/dist/host/storage/providers/registry.js +14 -0
- package/dist/host/storage/providers/types.d.ts +33 -0
- package/dist/host/storage/providers/types.js +1 -0
- package/dist/host/tools/conpty.js +3 -2
- package/dist/host/tools/windows-terminal.js +8 -7
- package/dist/host/vpn/index.d.ts +1 -1
- package/dist/host/vpn/index.js +1 -1
- package/dist/host/vpn/prepare.d.ts +2 -0
- package/dist/host/vpn/prepare.js +63 -2
- package/dist/host/web/browser.d.ts +2 -0
- package/dist/host/web/browser.js +14 -1
- package/dist/host/web/index.d.ts +3 -2
- package/dist/host/web/index.js +3 -2
- package/dist/host/web/rpc.d.ts +24 -0
- package/dist/host/web/rpc.js +149 -0
- package/dist/host/web/runner.d.ts +2 -0
- package/dist/host/web/runner.js +1 -2
- package/dist/host/web/server.d.ts +7 -0
- package/dist/host/web/server.js +23 -3
- package/dist/runtime/agent.js +83 -18
- package/dist/runtime/daemon.js +1805 -1221
- package/dist/runtime/db.js +83 -18
- package/dist/runtime/main.js +779 -219
- package/dist/runtime/netproxy.js +73 -11
- package/dist/runtime/view.js +91 -16
- package/dist/selftest/agent-paths.test.js +11 -1
- package/dist/selftest/app.test.d.ts +2 -0
- package/dist/selftest/app.test.js +324 -0
- package/dist/selftest/channel.test.js +13 -0
- package/dist/selftest/cli.test.js +2 -1
- package/dist/selftest/config.test.js +59 -0
- package/dist/selftest/contained.test.d.ts +2 -0
- package/dist/selftest/contained.test.js +544 -0
- package/dist/selftest/credentials.test.js +1 -1
- package/dist/selftest/engine.test.d.ts +2 -0
- package/dist/selftest/engine.test.js +127 -0
- package/dist/selftest/guest-boot.test.js +47 -1
- package/dist/selftest/hcs.test.d.ts +2 -0
- package/dist/selftest/hcs.test.js +482 -0
- package/dist/selftest/heavy-load.test.d.ts +2 -0
- package/dist/selftest/heavy-load.test.js +355 -0
- package/dist/selftest/index.js +12 -0
- package/dist/selftest/layout.test.js +12 -0
- package/dist/selftest/menu-perf.test.d.ts +2 -0
- package/dist/selftest/menu-perf.test.js +248 -0
- package/dist/selftest/menu-screens.js +4 -2
- package/dist/selftest/menu.test.js +36 -8
- package/dist/selftest/network.test.js +20 -1
- package/dist/selftest/support/engine-cases.d.ts +8 -0
- package/dist/selftest/support/engine-cases.js +65 -0
- package/dist/selftest/support/engine-golden.d.ts +3 -0
- package/dist/selftest/support/engine-golden.js +66 -0
- package/dist/selftest/support/fixtures.d.ts +8 -1
- package/dist/selftest/support/hcs-probes.d.ts +17 -0
- package/dist/selftest/support/hcs-probes.js +41 -0
- package/dist/selftest/vpn.test.js +136 -3
- package/dist/selftest/web.test.js +101 -3
- package/dist/shared/channel/run-dir.d.ts +4 -1
- package/dist/shared/channel/run-dir.js +7 -1
- package/dist/shared/channel/schema.d.ts +35 -1
- package/dist/shared/channel/schema.js +27 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +6 -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/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +38 -0
- package/dist/shared/memory/policy.js +63 -0
- package/dist/shared/network/firewall-rules.d.ts +8 -0
- package/dist/shared/network/firewall-rules.js +28 -0
- package/dist/shared/pty/output-filter.d.ts +14 -1
- package/dist/shared/pty/output-filter.js +22 -4
- package/dist/shared/pty/terminal.d.ts +4 -1
- package/dist/shared/pty/terminal.js +4 -2
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/shared/vpn/download.d.ts +16 -0
- package/dist/shared/vpn/download.js +16 -0
- package/dist/shared/vpn/wireguard.d.ts +51 -0
- package/dist/shared/vpn/wireguard.js +227 -0
- package/dist/sync/sync-cycle.js +5 -5
- package/dist/sync/watcher.d.ts +13 -0
- package/dist/sync/watcher.js +92 -3
- package/dist/web/index.html +4 -3
- package/dist/web/main-NRRS5O6Z.js +157 -0
- package/dist/web/main-T5DMCHYV.css +1 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +14 -0
- package/native/csharp/HcsHost/Guard.cs +100 -0
- package/native/csharp/HcsHost/Hcs.cs +256 -0
- package/native/csharp/HcsHost/Json.cs +78 -0
- package/native/csharp/HcsHost/Program.cs +185 -0
- package/native/csharp/HcsHost/VirtualDisk.cs +95 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +14 -0
- package/native/csharp/HostTools/MemoryStatus.cs +128 -0
- package/native/csharp/HostTools/Program.cs +58 -17
- package/native/scripts/check-contained.ps1 +28 -0
- package/native/scripts/contained-common.ps1 +241 -0
- package/native/scripts/dismount-contained.ps1 +66 -0
- package/native/scripts/memory-status.ps1 +42 -0
- package/native/scripts/mount-contained.ps1 +139 -0
- package/native/scripts/run-elevated.ps1 +17 -4
- package/native/scripts/stop-events.ps1 +23 -8
- package/package.json +9 -2
- package/sandbox/csharp/MemGov.cs +466 -0
- package/sandbox/csharp/PtyHost.cs +111 -5
- package/sandbox/scripts/new-shortcuts.ps1 +3 -1
- package/schemas/config.schema.json +66 -8
- package/schemas/sandbox-config.schema.json +3 -3
- package/dist/cli/commands/sessions/control.d.ts +0 -4
- package/dist/cli/commands/sessions/control.js +0 -66
- package/dist/cli/commands/sessions/list.d.ts +0 -2
- package/dist/cli/commands/sessions/list.js +0 -48
- package/dist/cli/commands/sessions/status.d.ts +0 -4
- package/dist/cli/commands/sessions/status.js +0 -114
- package/dist/cli/commands/sessions/window.d.ts +0 -3
- package/dist/cli/commands/sessions/window.js +0 -18
- package/dist/cli/menu/settings/path-kind.js +0 -44
- package/dist/web/main-CXESL347.js +0 -45
- package/dist/web/main-VLR7DVQ4.css +0 -1
- /package/dist/{cli/menu/settings → app/kernel}/schema-node.d.ts +0 -0
- /package/dist/{cli/menu/settings → app/kernel}/schema-node.js +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const FAKE_IMAGE = {
|
|
2
|
+
id: 'sha256:' + 'a'.repeat(64),
|
|
3
|
+
layers: ['C:\\img\\base', 'C:\\img\\l2'],
|
|
4
|
+
utilityVmFiles: 'C:\\img\\base\\UtilityVM\\Files',
|
|
5
|
+
};
|
|
6
|
+
export const GOOD_PC = {
|
|
7
|
+
windows: true,
|
|
8
|
+
build: 22621,
|
|
9
|
+
computecore: true,
|
|
10
|
+
services: { vmcompute: true, cexecsvc: true },
|
|
11
|
+
token: { elevatedAdministrator: true, hyperVAdministrators: false },
|
|
12
|
+
image: FAKE_IMAGE,
|
|
13
|
+
};
|
|
14
|
+
export function fakeProbes(pc = {}) {
|
|
15
|
+
const p = { ...GOOD_PC, ...pc };
|
|
16
|
+
const calls = [];
|
|
17
|
+
const store = {
|
|
18
|
+
name: 'fake store',
|
|
19
|
+
find: () => {
|
|
20
|
+
calls.push('imageStore.find');
|
|
21
|
+
return { image: p.image, reason: p.image ? '' : 'The fake store has no image.' };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
calls,
|
|
26
|
+
isWindows: () => p.windows,
|
|
27
|
+
windowsBuild: () => p.build,
|
|
28
|
+
serviceInstalled: (name) => {
|
|
29
|
+
calls.push('serviceInstalled:' + name);
|
|
30
|
+
return name in p.services ? p.services[name] : null;
|
|
31
|
+
},
|
|
32
|
+
fileExists: (file) => {
|
|
33
|
+
calls.push('fileExists:' + file);
|
|
34
|
+
return file.toLowerCase().endsWith('computecore.dll') ? p.computecore : false;
|
|
35
|
+
},
|
|
36
|
+
tokenGroups: () => p.token,
|
|
37
|
+
imageStores: () => [store],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** A PC without the HCS pieces, for the tests that must not depend on the PC they run on. */
|
|
41
|
+
export const NO_HCS_PC = fakeProbes({ computecore: false, services: { vmcompute: false, cexecsvc: false } });
|
|
@@ -8,11 +8,12 @@ import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
|
|
|
8
8
|
import { SecretStore } from '../host/secrets/index.js';
|
|
9
9
|
import { clearSecrets } from '../host/secrets/index.js';
|
|
10
10
|
import { generateSandbox } from '../host/launch/index.js';
|
|
11
|
-
import { findCountry, nordProfile, prepareVpn, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
|
|
11
|
+
import { findCountry, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
|
|
12
12
|
import { RunDir } from '../shared/channel/index.js';
|
|
13
|
-
import { OPENVPN_MSI } from '../shared/vpn/download.js';
|
|
13
|
+
import { OPENVPN_MSI, WIREGUARD_MSI } from '../shared/vpn/download.js';
|
|
14
14
|
import { cleanServers, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
|
|
15
15
|
import { assertSafeOvpn, inspectOvpn, PROGRAM_DIRECTIVES } from '../shared/vpn/ovpn.js';
|
|
16
|
+
import { assertSafeWireguard, handshakeDone, inspectWireguard, renderWireguard, serviceRunning, WIREGUARD_PROGRAM_KEYS, wireguardEndpoints, wireguardTunnelAddress, } from '../shared/vpn/wireguard.js';
|
|
16
17
|
const PROFILE = [
|
|
17
18
|
'client',
|
|
18
19
|
'dev tun',
|
|
@@ -51,6 +52,24 @@ const PROFILE = [
|
|
|
51
52
|
'</tls-auth>',
|
|
52
53
|
'',
|
|
53
54
|
].join('\r\n');
|
|
55
|
+
const PRIVATE_KEY = 'PrivPlanted' + 'A'.repeat(32) + '=';
|
|
56
|
+
const PUBLIC_KEY = 'PubPlanted' + 'B'.repeat(33) + '=';
|
|
57
|
+
const PRESHARED_KEY = 'PskPlanted' + 'C'.repeat(33) + '=';
|
|
58
|
+
const WG_CONFIG = [
|
|
59
|
+
'[Interface]',
|
|
60
|
+
'PrivateKey = ' + PRIVATE_KEY,
|
|
61
|
+
'Address = 10.64.0.2/32, fd00::2/128',
|
|
62
|
+
'DNS = 10.64.0.1',
|
|
63
|
+
'MTU = 1380',
|
|
64
|
+
'',
|
|
65
|
+
'[Peer] # the office',
|
|
66
|
+
'PublicKey = ' + PUBLIC_KEY,
|
|
67
|
+
'PresharedKey = ' + PRESHARED_KEY,
|
|
68
|
+
'AllowedIPs = 0.0.0.0/0, ::/0',
|
|
69
|
+
'Endpoint = vpn.example.com:51820',
|
|
70
|
+
'PersistentKeepalive = 25',
|
|
71
|
+
'',
|
|
72
|
+
].join('\r\n');
|
|
54
73
|
const PASSWORD = 'vpn-pass-planted-91c2d7';
|
|
55
74
|
const USERNAME = 'vpn-user-planted-5e8a40';
|
|
56
75
|
export const vpnSuite = {
|
|
@@ -206,7 +225,7 @@ export const vpnSuite = {
|
|
|
206
225
|
});
|
|
207
226
|
t.check(prepared !== null &&
|
|
208
227
|
prepared.session.server === 'uk2201.nordvpn.com' &&
|
|
209
|
-
prepared.credentials
|
|
228
|
+
prepared.credentials?.password === PASSWORD, 'a launch picks the server, checks its profile and holds the login in memory');
|
|
210
229
|
await t.throws(() => prepareVpn({
|
|
211
230
|
config: run.config,
|
|
212
231
|
storageRoot: config.storage.root,
|
|
@@ -275,5 +294,119 @@ export const vpnSuite = {
|
|
|
275
294
|
killSwitch: true,
|
|
276
295
|
updatedAt: 'n',
|
|
277
296
|
}).text.includes('\u001b'), 'text the sandbox wrote loses its escape sequences before it is printed');
|
|
297
|
+
t.section('WireGuard: your own config is untrusted data');
|
|
298
|
+
const wg = inspectWireguard(WG_CONFIG);
|
|
299
|
+
t.check(wg.ok &&
|
|
300
|
+
wg.config.peers.length === 1 &&
|
|
301
|
+
wg.config.peers[0].endpointHost === 'vpn.example.com' &&
|
|
302
|
+
wg.config.peers[0].endpointPort === 51820 &&
|
|
303
|
+
wireguardTunnelAddress(wg.config) === '10.64.0.2' &&
|
|
304
|
+
wireguardEndpoints(wg.config)[0].port === 51820, 'a plain config is accepted and says where it connects and which address the tunnel has', JSON.stringify(wg));
|
|
305
|
+
for (const key of WIREGUARD_PROGRAM_KEYS) {
|
|
306
|
+
const bad = inspectWireguard(WG_CONFIG.replace('MTU = 1380', key + ' = calc.exe'));
|
|
307
|
+
t.check(!bad.ok && bad.problems.some((p) => p.toLowerCase().includes(key)), key + ' is refused by name', JSON.stringify(bad));
|
|
308
|
+
}
|
|
309
|
+
const wgOdd = [
|
|
310
|
+
['FwMark = 7', 'an unknown [Interface] key is refused, not passed on'],
|
|
311
|
+
['postup = calc', 'lower case does not hide PostUp'],
|
|
312
|
+
['[Socks]', 'an unknown section is refused'],
|
|
313
|
+
['DNS = evil.example.com', 'a DNS name (a search domain) is refused: only addresses'],
|
|
314
|
+
['MTU = 99999', 'an MTU out of range is refused'],
|
|
315
|
+
['PrivateKey = ' + PRIVATE_KEY, 'a second PrivateKey is refused'],
|
|
316
|
+
];
|
|
317
|
+
for (const [line, why] of wgOdd) {
|
|
318
|
+
const text = line.startsWith('[') ? WG_CONFIG + line + '\r\n' : WG_CONFIG.replace('MTU = 1380', line);
|
|
319
|
+
t.check(!inspectWireguard(text).ok, why, line);
|
|
320
|
+
}
|
|
321
|
+
t.check(!inspectWireguard(WG_CONFIG.replace(PRIVATE_KEY, 'tooshort')).ok &&
|
|
322
|
+
!inspectWireguard(WG_CONFIG.replace('10.64.0.2/32, fd00::2/128', 'fd00::2/128')).ok &&
|
|
323
|
+
!inspectWireguard(WG_CONFIG.replace('vpn.example.com:51820', '[2001:db8::1]:51820')).ok &&
|
|
324
|
+
!inspectWireguard(WG_CONFIG.replace('Endpoint = vpn.example.com:51820', '')).ok &&
|
|
325
|
+
!inspectWireguard(WG_CONFIG.replace('vpn.example.com:51820', 'vpn.example.com:0')).ok &&
|
|
326
|
+
!inspectWireguard(WG_CONFIG.replace('[Peer] # the office', '')).ok &&
|
|
327
|
+
!inspectWireguard('x'.repeat(20 * 1024)).ok &&
|
|
328
|
+
!inspectWireguard(WG_CONFIG.replace('AllowedIPs = 0.0.0.0/0, ::/0', 'AllowedIPs = 300.0.0.0/0')).ok, 'a bad key, an IPv6-only address or endpoint, a missing endpoint or peer, a bad port, a huge file and a bad range are refused');
|
|
329
|
+
t.check(inspectWireguard(WG_CONFIG.replace('MTU = 1380', '# PostUp = calc\r\nMTU = 1380 # PreUp = calc')).ok, 'comments are comments (a commented-out PostUp is not a key)');
|
|
330
|
+
const refusals = [
|
|
331
|
+
WG_CONFIG.replace('MTU = 1380', 'PostUp = ' + PRIVATE_KEY),
|
|
332
|
+
WG_CONFIG.replace('MTU = 1380', 'Bogus = ' + PRESHARED_KEY),
|
|
333
|
+
WG_CONFIG.replace(PUBLIC_KEY, 'bad' + PRESHARED_KEY),
|
|
334
|
+
WG_CONFIG.replace(PRIVATE_KEY, PRIVATE_KEY + 'x'),
|
|
335
|
+
].map(inspectWireguard);
|
|
336
|
+
t.check(refusals.every((r) => !r.ok && !JSON.stringify(r).includes('PrivPlanted') && !JSON.stringify(r).includes('PskPlanted')), 'what a refusal says never contains a key');
|
|
337
|
+
await t.throws(() => assertSafeWireguard(WG_CONFIG.replace('MTU = 1380', 'PostUp = calc'), 'office.conf'), 'office.conf was refused', 'a refused config names its file and the reason');
|
|
338
|
+
if (wg.ok) {
|
|
339
|
+
const text = renderWireguard(wg.config, { 0: '203.0.113.7' });
|
|
340
|
+
t.check(text.includes('Endpoint = 203.0.113.7:51820') &&
|
|
341
|
+
!text.includes('vpn.example.com') &&
|
|
342
|
+
!text.includes('the office') &&
|
|
343
|
+
inspectWireguard(text).ok, 'the config handed on is written again from the checked values, with the endpoint as an address, and is itself valid');
|
|
344
|
+
}
|
|
345
|
+
const wgRules = killSwitchCommands([{ ip: '203.0.113.7', port: 51820, proto: 'udp' }], null).map((c) => c.join(' '));
|
|
346
|
+
t.check(wgRules.some((c) => c.includes('name=AgentSandbox-VPN-Server-1') &&
|
|
347
|
+
c.includes('protocol=udp') &&
|
|
348
|
+
c.includes('remoteip=203.0.113.7') &&
|
|
349
|
+
c.includes('remoteport=51820')) && wgRules.findIndex((c) => c.includes('blockoutbound')) < wgRules.findIndex((c) => c.includes('action=allow')), 'the kill switch blocks first and lets only the endpoint host:port through, over udp');
|
|
350
|
+
t.check(tunnelCommands('10.64.0.2').some((c) => c.join(' ').includes('localip=10.64.0.2') && c[2] === 'add'), "the tunnel's own address (from Address) is let through once the service runs");
|
|
351
|
+
t.check(WIREGUARD_MSI.x64.sha256.length === 64 &&
|
|
352
|
+
WIREGUARD_MSI.arm64.sha256.length === 64 &&
|
|
353
|
+
WIREGUARD_MSI.base.startsWith('https://download.wireguard.com/') &&
|
|
354
|
+
serviceRunning('STATE : 4 RUNNING') &&
|
|
355
|
+
!serviceRunning('STATE : 1 STOPPED') &&
|
|
356
|
+
handshakeDone('abc=\t1700000000\r\n') &&
|
|
357
|
+
!handshakeDone('abc=\t0\r\n') &&
|
|
358
|
+
!handshakeDone(''), 'the WireGuard download is pinned by checksum; the service state and handshake are read from the tools output');
|
|
359
|
+
t.section('WireGuard: the keys never land in session.json or the launch folder');
|
|
360
|
+
const wgDir = t.dir('wg-config');
|
|
361
|
+
const wgFile = path.join(wgDir, 'office.conf');
|
|
362
|
+
fs.writeFileSync(wgFile, WG_CONFIG);
|
|
363
|
+
const wgSettings = f.makeConfig();
|
|
364
|
+
wgSettings.vpn = { ...wgSettings.vpn, enabled: true, provider: 'wireguard-file', configFile: wgFile };
|
|
365
|
+
const wgRun = await f.run({}, wgSettings);
|
|
366
|
+
const wgPrepared = await prepareVpn({
|
|
367
|
+
config: wgRun.config,
|
|
368
|
+
storageRoot: wgSettings.storage.root,
|
|
369
|
+
login: new VpnLogin(t.dir('wg-nologin'), SecretStore.memory()),
|
|
370
|
+
});
|
|
371
|
+
t.check(wgPrepared !== null &&
|
|
372
|
+
wgPrepared.session.server === 'office.conf' &&
|
|
373
|
+
wgPrepared.session.profile === undefined &&
|
|
374
|
+
wgPrepared.credentials === undefined &&
|
|
375
|
+
(wgPrepared.wireguardConfig ?? '').includes(PRIVATE_KEY), 'a launch needs no saved login; it holds the checked config (with the private key) in memory only');
|
|
376
|
+
fs.writeFileSync(wgFile, WG_CONFIG.replace('MTU = 1380', 'PostUp = calc.exe'));
|
|
377
|
+
await t.throws(() => prepareVpn({ config: wgRun.config, storageRoot: wgSettings.storage.root }), 'refused', 'a config with PostUp stops the launch before anything is written');
|
|
378
|
+
t.check(wireguardFileProblem(wgFile, wgDir)?.includes('refused') === true &&
|
|
379
|
+
wireguardFileProblem(path.join(wgDir, 'missing.conf'), wgDir) !== null &&
|
|
380
|
+
!(wireguardFileProblem(wgFile, wgDir) ?? '').includes('PrivPlanted'), 'doctor says a bad or missing config is a problem, without a key');
|
|
381
|
+
fs.writeFileSync(wgFile, WG_CONFIG);
|
|
382
|
+
t.check(wireguardFileProblem('office.conf', wgDir) === null, 'a good config, even by a relative path, is fine');
|
|
383
|
+
clearSecrets(wgSettings.storage.root, wgRun.dataPath);
|
|
384
|
+
const wgLaunch = generateSandbox({
|
|
385
|
+
packageRoot: t.packageRoot,
|
|
386
|
+
run: wgRun,
|
|
387
|
+
harnessArgs: [],
|
|
388
|
+
probe: f.probe,
|
|
389
|
+
pcLanguages: ['python', 'rust', 'cpp', 'dotnet'],
|
|
390
|
+
...(wgPrepared ? { vpn: wgPrepared } : {}),
|
|
391
|
+
});
|
|
392
|
+
const wgSession = fs.readFileSync(wgLaunch.sessionPath, 'utf8');
|
|
393
|
+
const wgAll = [];
|
|
394
|
+
const wgWalk = (d) => {
|
|
395
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
396
|
+
const p = path.join(d, e.name);
|
|
397
|
+
if (e.isDirectory())
|
|
398
|
+
wgWalk(p);
|
|
399
|
+
else
|
|
400
|
+
wgAll.push(fs.readFileSync(p, 'latin1'));
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
wgWalk(wgLaunch.launchPath);
|
|
404
|
+
const hasKey = (text) => text.includes(PRIVATE_KEY) || text.includes(PRESHARED_KEY) || text.includes('PrivPlanted');
|
|
405
|
+
t.check(!hasKey(wgSession) && !wgAll.some(hasKey) && !fs.existsSync(path.join(wgLaunch.launchPath, 'vpn')), 'neither the private key nor the preshared key is in session.json or any file of the launch folder');
|
|
406
|
+
const wgHanded = f.readJson(new RunDir(wgRun.dataPath).secretFile(wgLaunch.launchId));
|
|
407
|
+
t.check((wgHanded.wireguardConfig ?? '').includes(PRIVATE_KEY) && wgHanded.vpn === undefined, 'the config is only in the one-shot file for the sandbox');
|
|
408
|
+
clearSecrets(wgSettings.storage.root, wgRun.dataPath);
|
|
409
|
+
t.check(!fs.existsSync(new RunDir(wgRun.dataPath).secretFile(wgLaunch.launchId)) &&
|
|
410
|
+
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');
|
|
278
411
|
},
|
|
279
412
|
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// The web UI's server: login and cookie, the Host / Origin / CSRF checks, static files that cannot leave their folder,
|
|
2
|
-
// sessions without tokens,
|
|
3
|
-
// backend's, so the fake is only reachable here).
|
|
2
|
+
// sessions without tokens, the terminal bridge against a fake ptyhost on loopback (the real address check is the
|
|
3
|
+
// backend's, so the fake is only reachable here), and the RPC socket of the shared core against a fake RpcHost.
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import WebSocket from 'ws';
|
|
8
|
+
import { cancelled } from '../app/kernel/index.js';
|
|
8
9
|
import { webSession } from '../cli/services/index.js';
|
|
10
|
+
import { fail } from '../core/util/index.js';
|
|
9
11
|
import { cookieValue, csrfOk, hostAllowed, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
|
|
10
12
|
import { encodeFrame, FrameReader, FrameType } from '../shared/pty/index.js';
|
|
11
13
|
import { CSRF_HEADER, CSRF_VALUE, parseClientMessage } from '../shared/web/index.js';
|
|
@@ -96,6 +98,35 @@ class Tab {
|
|
|
96
98
|
this.socket.on('close', (code) => (this.closeCode = code));
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
/** A stand-in for the shared core: one operation that reports, asks and may fail, and one live query. */
|
|
102
|
+
function fakeRpc() {
|
|
103
|
+
const ran = [];
|
|
104
|
+
const live = { value: 1 };
|
|
105
|
+
const host = {
|
|
106
|
+
kind: (id) => (id === 'test.work' ? 'operation' : id === 'test.live' ? 'query' : undefined),
|
|
107
|
+
liveMs: (id) => (id === 'test.live' ? 50 : undefined),
|
|
108
|
+
run: async (id, input, io) => {
|
|
109
|
+
ran.push(id);
|
|
110
|
+
if (id === 'test.live')
|
|
111
|
+
return { value: live.value };
|
|
112
|
+
const how = (input ?? {});
|
|
113
|
+
io.progress('Working');
|
|
114
|
+
io.log('one line', 'muted');
|
|
115
|
+
if (!(await io.confirm({ title: 'Go on?' })))
|
|
116
|
+
cancelled();
|
|
117
|
+
if (how.fail)
|
|
118
|
+
fail('It broke.', 'Fix it like this.');
|
|
119
|
+
if (how.bug)
|
|
120
|
+
throw new Error('internal detail C:\\Users\\secret');
|
|
121
|
+
return { done: true };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return { host, ran, live };
|
|
125
|
+
}
|
|
126
|
+
/** The server's messages on an RPC socket, parsed. */
|
|
127
|
+
function rpcMessages(tab) {
|
|
128
|
+
return tab.texts.map((text) => JSON.parse(text));
|
|
129
|
+
}
|
|
99
130
|
const session = (id, attach) => ({
|
|
100
131
|
id,
|
|
101
132
|
launchId: 'launch1',
|
|
@@ -202,7 +233,15 @@ export const webSuite = {
|
|
|
202
233
|
},
|
|
203
234
|
};
|
|
204
235
|
let stopped = false;
|
|
205
|
-
const
|
|
236
|
+
const rpc = fakeRpc();
|
|
237
|
+
const server = new WebServer({
|
|
238
|
+
port: 0,
|
|
239
|
+
token: TOKEN,
|
|
240
|
+
backend,
|
|
241
|
+
staticRoot: root,
|
|
242
|
+
rpc: rpc.host,
|
|
243
|
+
onStop: () => (stopped = true),
|
|
244
|
+
});
|
|
206
245
|
await server.start();
|
|
207
246
|
const { port } = server;
|
|
208
247
|
const origin = 'http://127.0.0.1:' + port;
|
|
@@ -282,6 +321,65 @@ export const webSuite = {
|
|
|
282
321
|
leaving.socket.close();
|
|
283
322
|
await t.waitFor(() => fake.closed >= 3, 5000);
|
|
284
323
|
t.check(fake.closed >= 3, 'closing the browser side closes the connection to the session');
|
|
324
|
+
t.section('the RPC socket');
|
|
325
|
+
const rpcUrl = 'ws://127.0.0.1:' + port + '/api/rpc';
|
|
326
|
+
const refusedRpc = [
|
|
327
|
+
new Tab(rpcUrl, { Cookie }),
|
|
328
|
+
new Tab(rpcUrl, { Origin: 'http://evil.example', Cookie }),
|
|
329
|
+
new Tab(rpcUrl, { Origin: origin }),
|
|
330
|
+
];
|
|
331
|
+
await t.waitFor(() => refusedRpc.every((a) => a.failed), 5000);
|
|
332
|
+
t.check(refusedRpc.every((a) => !a.opened), 'the RPC socket needs the cookie and this page as its Origin, like the terminal');
|
|
333
|
+
const browser = new Tab(rpcUrl, { Origin: origin, Cookie });
|
|
334
|
+
await t.waitFor(() => browser.opened, 5000);
|
|
335
|
+
const reply = (id, kind) => rpcMessages(browser).find((m) => m.id === id && m.t === kind);
|
|
336
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'c1', op: 'test.work', input: {} }));
|
|
337
|
+
await t.waitFor(() => reply('c1', 'ask'), 5000);
|
|
338
|
+
t.check(reply('c1', 'progress') !== undefined && reply('c1', 'log') !== undefined, 'a call streams its progress and log lines before it asks');
|
|
339
|
+
browser.socket.send(JSON.stringify({ t: 'answer', id: 'c1', value: true }));
|
|
340
|
+
await t.waitFor(() => reply('c1', 'result'), 5000);
|
|
341
|
+
t.equal(reply('c1', 'result'), { t: 'result', id: 'c1', output: { done: true } }, 'the answer lets it finish');
|
|
342
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'c2', op: 'test.work', input: {} }));
|
|
343
|
+
await t.waitFor(() => reply('c2', 'ask'), 5000);
|
|
344
|
+
browser.socket.send(JSON.stringify({ t: 'answer', id: 'c2', value: 'yes please' }));
|
|
345
|
+
await t.waitFor(() => reply('c2', 'error'), 5000);
|
|
346
|
+
const declinedCall = reply('c2', 'error');
|
|
347
|
+
t.check(declinedCall?.t === 'error' && declinedCall.error.kind === 'cancelled', 'an answer of the wrong kind counts as no: nothing changes');
|
|
348
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'c3', op: 'test.work', input: { fail: true } }));
|
|
349
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'c4', op: 'test.work', input: { bug: true } }));
|
|
350
|
+
await t.waitFor(() => reply('c3', 'ask') && reply('c4', 'ask'), 5000);
|
|
351
|
+
browser.socket.send(JSON.stringify({ t: 'answer', id: 'c3', value: true }));
|
|
352
|
+
browser.socket.send(JSON.stringify({ t: 'answer', id: 'c4', value: true }));
|
|
353
|
+
await t.waitFor(() => reply('c3', 'error') && reply('c4', 'error'), 5000);
|
|
354
|
+
t.equal(reply('c3', 'error'), { t: 'error', id: 'c3', error: { kind: 'user', message: 'It broke.', fix: 'Fix it like this.' } }, 'a failure arrives as its message and fix');
|
|
355
|
+
t.equal(reply('c4', 'error'), { t: 'error', id: 'c4', error: { kind: 'bug', message: 'Something went wrong.' } }, "a bug's details stay on this PC");
|
|
356
|
+
const before = rpc.ran.length;
|
|
357
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'c5', op: 'sandbox.notThere', input: {} }));
|
|
358
|
+
await t.waitFor(() => reply('c5', 'error'), 5000);
|
|
359
|
+
const unknown = reply('c5', 'error');
|
|
360
|
+
t.check(unknown?.t === 'error' && unknown.error.kind === 'invalid' && rpc.ran.length === before, 'an operation that is not allowed on the web is never run');
|
|
361
|
+
browser.socket.send(JSON.stringify({ t: 'subscribe', id: 's1', query: 'test.live', input: {} }));
|
|
362
|
+
await t.waitFor(() => reply('s1', 'data'), 5000);
|
|
363
|
+
rpc.live.value = 2;
|
|
364
|
+
await t.waitFor(() => rpcMessages(browser).filter((m) => m.id === 's1' && m.t === 'data').length === 2, 5000);
|
|
365
|
+
const updates = rpcMessages(browser).filter((m) => m.id === 's1' && m.t === 'data');
|
|
366
|
+
t.equal(updates.map((m) => (m.t === 'data' ? m.output : null)), [{ value: 1 }, { value: 2 }], 'a live query sends its result, then again only when it changed');
|
|
367
|
+
browser.socket.send(JSON.stringify({ t: 'unsubscribe', id: 's1' }));
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
369
|
+
const reads = rpc.ran.filter((id) => id === 'test.live').length;
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
371
|
+
t.equal(rpc.ran.filter((id) => id === 'test.live').length, reads, 'unsubscribing stops the reads');
|
|
372
|
+
browser.socket.send(JSON.stringify({ t: 'call', id: 'bad id!', op: 'test.work', input: {} }));
|
|
373
|
+
await t.waitFor(() => browser.closeCode !== null, 5000);
|
|
374
|
+
t.equal(browser.closeCode, 1008, 'a message that does not fit the schema closes the socket');
|
|
375
|
+
const leaver = new Tab(rpcUrl, { Origin: origin, Cookie });
|
|
376
|
+
await t.waitFor(() => leaver.opened, 5000);
|
|
377
|
+
const runs = rpc.ran.length;
|
|
378
|
+
leaver.socket.send(JSON.stringify({ t: 'call', id: 'c1', op: 'test.work', input: {} }));
|
|
379
|
+
await t.waitFor(() => leaver.texts.some((m) => m.includes('"ask"')), 5000);
|
|
380
|
+
leaver.socket.close();
|
|
381
|
+
await t.waitFor(() => leaver.closeCode !== null, 5000);
|
|
382
|
+
t.check(rpc.ran.length === runs + 1, 'a browser that leaves mid-question answers no (the call ends)');
|
|
285
383
|
t.section('stopping');
|
|
286
384
|
t.equal((await send(port, 'POST', '/api/stop', { ...json, Cookie }, '{}')).status, 403, 'stop needs CSRF and Origin too');
|
|
287
385
|
t.equal((await send(port, 'POST', '/api/stop', { ...ajax, Cookie }, '{}')).status, 200, 'a signed-in browser can stop it');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GuestRequest, GuestResponse, ProjectStatus, SandboxStatus, SessionStatus, VpnStatus } from './schema.js';
|
|
1
|
+
import type { GuestRequest, GuestResponse, PressureStatus, ProjectStatus, SandboxStatus, SessionStatus, VpnStatus } from './schema.js';
|
|
2
2
|
/** Ids become file names, so only a strict alphabet is allowed (no separators, no dots). */
|
|
3
3
|
export declare function safeId(id: string): string;
|
|
4
4
|
export declare function now(): string;
|
|
@@ -7,6 +7,8 @@ export declare class RunDir {
|
|
|
7
7
|
readonly root: string;
|
|
8
8
|
readonly sandboxFile: string;
|
|
9
9
|
readonly vpnFile: string;
|
|
10
|
+
/** What the memory governor in the sandbox is doing (pressure.json). */
|
|
11
|
+
readonly pressureFile: string;
|
|
10
12
|
readonly requestsDir: string;
|
|
11
13
|
readonly responsesDir: string;
|
|
12
14
|
readonly sessionsDir: string;
|
|
@@ -23,6 +25,7 @@ export declare class RunDir {
|
|
|
23
25
|
secretFile(id: string): string;
|
|
24
26
|
readSandboxStatus(): SandboxStatus | null;
|
|
25
27
|
readVpnStatus(): VpnStatus | null;
|
|
28
|
+
readPressure(): PressureStatus | null;
|
|
26
29
|
readSession(id: string): SessionStatus | null;
|
|
27
30
|
readResponse(id: string): GuestResponse | null;
|
|
28
31
|
listSessions(): SessionStatus[];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// written atomically and parsed with the channel schemas on read. Anything that does not parse is ignored.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { GuestRequestSchema, GuestResponseSchema, IdSchema, ProjectStatusSchema, SandboxStatusSchema, SessionStatusSchema, VpnStatusSchema, } from './schema.js';
|
|
6
|
+
import { GuestRequestSchema, GuestResponseSchema, IdSchema, PressureStatusSchema, ProjectStatusSchema, SandboxStatusSchema, SessionStatusSchema, VpnStatusSchema, } from './schema.js';
|
|
7
7
|
import { readValid } from '../io/index.js';
|
|
8
8
|
/** Ids become file names, so only a strict alphabet is allowed (no separators, no dots). */
|
|
9
9
|
export function safeId(id) {
|
|
@@ -34,6 +34,8 @@ export class RunDir {
|
|
|
34
34
|
root;
|
|
35
35
|
sandboxFile;
|
|
36
36
|
vpnFile;
|
|
37
|
+
/** What the memory governor in the sandbox is doing (pressure.json). */
|
|
38
|
+
pressureFile;
|
|
37
39
|
requestsDir;
|
|
38
40
|
responsesDir;
|
|
39
41
|
sessionsDir;
|
|
@@ -46,6 +48,7 @@ export class RunDir {
|
|
|
46
48
|
this.root = path.join(dataDir, 'run');
|
|
47
49
|
this.sandboxFile = path.join(this.root, 'sandbox.json');
|
|
48
50
|
this.vpnFile = path.join(this.root, 'vpn.json');
|
|
51
|
+
this.pressureFile = path.join(this.root, 'pressure.json');
|
|
49
52
|
this.requestsDir = path.join(this.root, 'requests');
|
|
50
53
|
this.responsesDir = path.join(this.root, 'responses');
|
|
51
54
|
this.sessionsDir = path.join(this.root, 'sessions');
|
|
@@ -74,6 +77,9 @@ export class RunDir {
|
|
|
74
77
|
readVpnStatus() {
|
|
75
78
|
return readValid(VpnStatusSchema, this.vpnFile);
|
|
76
79
|
}
|
|
80
|
+
readPressure() {
|
|
81
|
+
return readValid(PressureStatusSchema, this.pressureFile);
|
|
82
|
+
}
|
|
77
83
|
readSession(id) {
|
|
78
84
|
return readValid(SessionStatusSchema, this.sessionFile(id));
|
|
79
85
|
}
|
|
@@ -194,6 +194,7 @@ export declare const ProjectSpecSchema: z.ZodObject<{
|
|
|
194
194
|
hostDir: z.ZodOptional<z.ZodString>;
|
|
195
195
|
}, z.core.$strict>>;
|
|
196
196
|
localState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
197
|
+
bootReset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
197
198
|
resumeArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
198
199
|
resumeOneArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
199
200
|
}, z.core.$strict>>;
|
|
@@ -351,6 +352,7 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
351
352
|
hostDir: z.ZodOptional<z.ZodString>;
|
|
352
353
|
}, z.core.$strict>>;
|
|
353
354
|
localState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
355
|
+
bootReset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
354
356
|
resumeArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
355
357
|
resumeOneArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
356
358
|
}, z.core.$strict>>;
|
|
@@ -372,6 +374,7 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
372
374
|
hostDir: z.ZodOptional<z.ZodString>;
|
|
373
375
|
}, z.core.$strict>>;
|
|
374
376
|
localState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
377
|
+
bootReset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
375
378
|
resumeArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
376
379
|
resumeOneArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
377
380
|
}, z.core.$strict>>;
|
|
@@ -524,6 +527,7 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
524
527
|
hostDir: z.ZodOptional<z.ZodString>;
|
|
525
528
|
}, z.core.$strict>>;
|
|
526
529
|
localState: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
530
|
+
bootReset: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
527
531
|
resumeArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
528
532
|
resumeOneArguments: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
529
533
|
}, z.core.$strict>>;
|
|
@@ -547,7 +551,37 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
547
551
|
kind: z.ZodLiteral<"ping">;
|
|
548
552
|
id: z.ZodString;
|
|
549
553
|
createdAt: z.ZodString;
|
|
554
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
555
|
+
kind: z.ZodLiteral<"pressure">;
|
|
556
|
+
id: z.ZodString;
|
|
557
|
+
reason: z.ZodString;
|
|
558
|
+
seconds: z.ZodNumber;
|
|
559
|
+
createdAt: z.ZodString;
|
|
550
560
|
}, z.core.$strict>], "kind">;
|
|
561
|
+
/** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
|
|
562
|
+
export declare const PressureStatusSchema: z.ZodObject<{
|
|
563
|
+
level: z.ZodEnum<{
|
|
564
|
+
ok: "ok";
|
|
565
|
+
throttled: "throttled";
|
|
566
|
+
suspending: "suspending";
|
|
567
|
+
}>;
|
|
568
|
+
profile: z.ZodEnum<{
|
|
569
|
+
normal: "normal";
|
|
570
|
+
gentle: "gentle";
|
|
571
|
+
}>;
|
|
572
|
+
ramFreePercent: z.ZodNumber;
|
|
573
|
+
commitPercent: z.ZodNumber;
|
|
574
|
+
lowered: z.ZodNumber;
|
|
575
|
+
paused: z.ZodArray<z.ZodObject<{
|
|
576
|
+
name: z.ZodString;
|
|
577
|
+
pid: z.ZodNumber;
|
|
578
|
+
seconds: z.ZodNumber;
|
|
579
|
+
}, z.core.$strip>>;
|
|
580
|
+
pcLow: z.ZodBoolean;
|
|
581
|
+
lastAction: z.ZodString;
|
|
582
|
+
updatedAt: z.ZodString;
|
|
583
|
+
}, z.core.$strip>;
|
|
584
|
+
export type PressureStatus = z.output<typeof PressureStatusSchema>;
|
|
551
585
|
/** C:\AgentData\run\responses\<id>.json: the guest's reply. */
|
|
552
586
|
export declare const GuestResponseSchema: z.ZodObject<{
|
|
553
587
|
id: z.ZodString;
|
|
@@ -604,9 +638,9 @@ export declare const ProjectStatusSchema: z.ZodObject<{
|
|
|
604
638
|
/** The guest writes run/vpn.json: whether the VPN tunnel is up (validated and capped on the host). */
|
|
605
639
|
export declare const VpnStatusSchema: z.ZodObject<{
|
|
606
640
|
state: z.ZodEnum<{
|
|
641
|
+
off: "off";
|
|
607
642
|
failed: "failed";
|
|
608
643
|
starting: "starting";
|
|
609
|
-
off: "off";
|
|
610
644
|
up: "up";
|
|
611
645
|
}>;
|
|
612
646
|
message: z.ZodString;
|
|
@@ -91,7 +91,34 @@ export const GuestRequestSchema = z.discriminatedUnion('kind', [
|
|
|
91
91
|
createdAt: TimeSchema,
|
|
92
92
|
}),
|
|
93
93
|
z.strictObject({ kind: z.literal('ping'), id: IdSchema, createdAt: TimeSchema }),
|
|
94
|
+
/**
|
|
95
|
+
* The PC's own memory or CPU is running low (the host watchdog): the sandbox slows its heaviest builds down for
|
|
96
|
+
* `seconds` (it never stops them) so Windows does not have to close it. Repeated while the PC stays low.
|
|
97
|
+
*/
|
|
98
|
+
z.strictObject({
|
|
99
|
+
kind: z.literal('pressure'),
|
|
100
|
+
id: IdSchema,
|
|
101
|
+
reason: Text(300),
|
|
102
|
+
seconds: z.number().int().min(10).max(300),
|
|
103
|
+
createdAt: TimeSchema,
|
|
104
|
+
}),
|
|
94
105
|
]);
|
|
106
|
+
/** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
|
|
107
|
+
export const PressureStatusSchema = z.object({
|
|
108
|
+
level: z.enum(['ok', 'throttled', 'suspending']),
|
|
109
|
+
profile: z.enum(['normal', 'gentle']),
|
|
110
|
+
/** Share of the sandbox's RAM still free and of its commit limit in use (percent). */
|
|
111
|
+
ramFreePercent: z.number().int().min(0).max(100),
|
|
112
|
+
commitPercent: z.number().int().min(0).max(100),
|
|
113
|
+
/** Programs running at lower priority because of it. */
|
|
114
|
+
lowered: z.number().int().min(0).max(1024),
|
|
115
|
+
/** Programs paused right now (name, process id, seconds left). */
|
|
116
|
+
paused: z.array(z.object({ name: Text(64), pid: z.number().int(), seconds: z.number().int().min(0).max(60) })).max(8),
|
|
117
|
+
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
118
|
+
pcLow: z.boolean(),
|
|
119
|
+
lastAction: Text(300),
|
|
120
|
+
updatedAt: TimeSchema,
|
|
121
|
+
});
|
|
95
122
|
/** C:\AgentData\run\responses\<id>.json: the guest's reply. */
|
|
96
123
|
export const GuestResponseSchema = z.object({
|
|
97
124
|
id: IdSchema,
|
|
@@ -8,6 +8,7 @@ export declare const SecretsFileSchema: z.ZodObject<{
|
|
|
8
8
|
username: z.ZodString;
|
|
9
9
|
password: z.ZodString;
|
|
10
10
|
}, z.core.$strict>>;
|
|
11
|
+
wireguardConfig: z.ZodOptional<z.ZodString>;
|
|
11
12
|
}, z.core.$strict>;
|
|
12
13
|
export type SecretsFile = z.output<typeof SecretsFileSchema>;
|
|
13
14
|
interface WithHarnesses {
|
|
@@ -19,6 +19,12 @@ export const SecretsFileSchema = z.strictObject({
|
|
|
19
19
|
.optional(),
|
|
20
20
|
/** VPN service credentials (vpn.provider's login), only for the launch's own sandbox. */
|
|
21
21
|
vpn: z.strictObject({ username: z.string().min(1).max(256), password: z.string().min(1).max(1024) }).optional(),
|
|
22
|
+
/** The checked WireGuard config of vpn.provider wireguard-file (holds the private key). */
|
|
23
|
+
wireguardConfig: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(32 * 1024)
|
|
27
|
+
.optional(),
|
|
22
28
|
});
|
|
23
29
|
/** The environment a harness gets: its own, then the values that travelled in the secrets file. */
|
|
24
30
|
const withEnvironment = (harness, environment) => (environment ? { ...harness, environment: { ...harness.environment, ...environment } } : harness);
|
|
@@ -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
|
+
}
|
package/dist/shared/io/index.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Exit code 8 and up means something could not be copied; below that every file is in place. */
|
|
2
|
+
export declare function robocopyFailed(code: number | null): boolean;
|
|
3
|
+
/** The exit code in plain words (it is a bit field: 1 copied, 2 extras, 4 mismatches, 8 failures, 16 fatal). */
|
|
4
|
+
export declare function robocopyMeaning(code: number | null): string;
|
|
5
|
+
/** The first few ERROR lines of robocopy's output (what went wrong and on which file), one line each. */
|
|
6
|
+
export declare function robocopyErrors(output: string, max?: number): string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Reading robocopy's exit code and output (pure; the guest and the tests share it).
|
|
2
|
+
/** Exit code 8 and up means something could not be copied; below that every file is in place. */
|
|
3
|
+
export function robocopyFailed(code) {
|
|
4
|
+
return code === null || code >= 8;
|
|
5
|
+
}
|
|
6
|
+
/** The exit code in plain words (it is a bit field: 1 copied, 2 extras, 4 mismatches, 8 failures, 16 fatal). */
|
|
7
|
+
export function robocopyMeaning(code) {
|
|
8
|
+
if (code === null)
|
|
9
|
+
return 'robocopy did not finish';
|
|
10
|
+
if (code >= 16)
|
|
11
|
+
return 'robocopy stopped with a fatal error (bad path or no access)';
|
|
12
|
+
if (code >= 8)
|
|
13
|
+
return code & 1 ? 'some files were copied and some could not be' : 'files could not be copied';
|
|
14
|
+
return code === 0 ? 'nothing to copy' : 'copied';
|
|
15
|
+
}
|
|
16
|
+
/** The first few ERROR lines of robocopy's output (what went wrong and on which file), one line each. */
|
|
17
|
+
export function robocopyErrors(output, max = 3) {
|
|
18
|
+
const lines = output
|
|
19
|
+
.split(/\r?\n/)
|
|
20
|
+
.map((l) => l.trim())
|
|
21
|
+
.filter((l) => /\bERROR\b/.test(l) || /^The (process|system) cannot/i.test(l));
|
|
22
|
+
return lines.slice(0, max).map((l) => (l.length > 220 ? l.slice(0, 217) + '...' : l));
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './policy.js';
|