Martin Hellspong 53fe06509e
Refuse to run the destructive ps1 test runner outside a sandbox (#343)
* test: refuse to run the destructive test runner outside a sandbox

tests/run_all_tests.ps1 kills all psmux processes and wipes ~/.psmux
(*.port, *.key, .psmux.conf, .psmuxrc) between tests. Run on a machine with
a live psmux it silently destroys the user's running sessions and config.

Gate it behind PSMUX_TEST_SANDBOX=1: the runner refuses (exit 2) with a clear
explanation unless that opt-in is set. The Docker dev image sets the variable
so the intended throwaway environment runs unchanged.

Adds tests/test_runner_safety_gate.ps1, a static guard (it reads the runner,
never executes it — safe anywhere, no binary needed) asserting the gate exists
and appears before every destructive operation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: anchor safety-gate guard on the real env check, not a comment

The static guard located the gate via the first textual occurrence of
PSMUX_TEST_SANDBOX, which is the explanatory comment rather than the
executable `if ($env:PSMUX_TEST_SANDBOX ...)` check. That made the guard
satisfiable by a comment naming the variable placed before destructive
code, even if the real gate sat below it. Anchor on the if-statement
instead so the position check reflects the actual abort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: guard kill-server in the safety-gate blacklist

run_all_tests.ps1 calls `& $PSMUX kill-server` in Clean-Server before the
Stop-Process cleanup, but the static guard did not list kill-server as a
destructive operation. A regression that moved the sandbox gate below the
kill-server call would have passed unnoticed. Add kill-server to the
blacklist so the position check covers it too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: flag any pre-gate Remove-Item, not just ~/.psmux deletes

The guard only recognised Remove-Item calls whose literal target was
"$env:USERPROFILE\.psmux...". Delete paths are commonly built from other
$env: vars, so a target-specific pattern would not see them as
destructive and a pre-gate delete could slip through. Match Remove-Item
broadly instead; almost nothing runs before the gate, so flagging any
pre-gate delete is the safe default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: match the safety gate and blacklist case-insensitively

[regex]::Match defaults to case-sensitive, but PowerShell keywords,
$env: lookups and cmdlet/command names are case-insensitive. Differently
cased code such as `remove-item` or `STOP-PROCESS` would therefore have
gone unnoticed by the guard. Pass RegexOptions::IgnoreCase to both the
gate-anchor match and the destructive-operation matches so casing cannot
change the verdict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: run the safety-gate guard from the PR smoke suite

The static guard lived as a standalone script that no automated path
invoked, so a removed or moved sandbox gate in run_all_tests.ps1 would
not be caught on a PR. CI already runs test_smoke_pr.ps1, so call the
guard from there: it only reads the runner (no binary or session), runs
before the session work, and a non-zero exit is reported as a smoke
failure. Per review discussion, wiring it into the smoke suite keeps the
check on the existing automated path and gives it local coverage too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:23:40 +05:30
..

psmux Docker Dev Environment

A Windows container with Rust (MSVC), Visual Studio Build Tools, and OpenSSH — ready to build and run psmux.

What's inside

Component Details
Base image mcr.microsoft.com/powershell:windowsservercore-ltsc2022
Rust stable-x86_64-pc-windows-msvc via rustup
MSVC Visual Studio Build Tools 2022 (cl.exe, link.exe)
SSH OpenSSH Server on port 2222 (key-only auth, no passwords)
Shell PowerShell 7 with auto-loaded VS dev environment
Git MinGit for cloning repos

Quick start

One command

pwsh -File docker\Run-PsmuxDev.ps1

This will:

  1. Generate an SSH key at ~/.ssh/psmux_docker_key (if not present)
  2. Build the Docker image (first time only)
  3. Start the container with your public key injected
  4. Print the SSH command to connect

Manual steps

1. Build the image

cd docker
docker build -t psmux-dev .

Note: The build takes a while (~15-30 min) because it downloads and installs Visual Studio Build Tools. The resulting image is large (~15 GB). This is expected for Windows MSVC containers.

2. Run the container

# Generate SSH key (once)
ssh-keygen -t ed25519 -f ~/.ssh/psmux_docker_key -N "" -C "psmux-docker"

# Run with your public key
$pubkey = Get-Content ~/.ssh/psmux_docker_key.pub
docker run -d --name psmux-dev --isolation=hyperv `
    -e "SSH_PUBLIC_KEY=$pubkey" `
    psmux-dev

3. SSH in

$ip = docker inspect psmux-dev --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
ssh -i ~/.ssh/psmux_docker_key -p 2222 ContainerAdministrator@$ip

4. Build psmux

git clone https://github.com/psmux/psmux.git
cd psmux
cargo install --path .
psmux --version

SSH authentication

This container uses key-only SSH — no passwords. Your public key is passed in via the SSH_PUBLIC_KEY environment variable at container start. The Run-PsmuxDev.ps1 script handles this automatically.

You can also mount a public key file:

docker run -d --name psmux-dev --isolation=hyperv `
    -v "$HOME\.ssh\id_ed25519.pub:C:\ssh_public_key" `
    psmux-dev

Verifying the toolchain

After SSH-ing in, these should all work:

rustc --version
cargo --version
where cl
where link

Safety notes

  • No passwords are used — SSH key auth only
  • Container runs with Hyper-V isolation (full VM separation from host)
  • SSH listens on port 2222 to avoid conflicts with host sshd
  • Key is stored at ~/.ssh/psmux_docker_key (never inside the repo)

File layout

docker/
  Dockerfile
  README.md
  Run-PsmuxDev.ps1               # Host-side: generates key, builds, runs, prints SSH command
  Tools/
    StartContainer.ps1            # Entrypoint: configures sshd with key auth, starts sshd
    InstallAll.ps1                # Build-time: installs Rust, VS Build Tools, OpenSSH, Git
    ImportVsDevEnv.ps1            # Loads VS dev environment (cl.exe, link.exe) into PowerShell
  Profile/
    Microsoft.PowerShell_profile.ps1   # Auto-loads Rust + MSVC env on every shell