Compare commits

..

No commits in common. "main" and "v0.4.0" have entirely different histories.
main ... v0.4.0

6 changed files with 113 additions and 2186 deletions

View file

@ -1,169 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.14.1] - 2025-01-25
### Changed
- Updated README.md with Windows skip parameters and VNC password info
- Updated CLAUDE.md with Windows script documentation and security patterns
- Fixed VNC password minimum documentation (6 → 8 chars)
## [0.14.0] - 2025-01-25
### Security
- Add SHA256 checksum verification for Ubuntu cloud image downloads (prevents MITM attacks)
- Add strict input validation for git name, email, VM password, and VNC password
- Validate loaded config.env values to detect tampering
- VNC password minimum increased from 6 to 8 characters
- Block shell metacharacters in all user inputs to prevent injection
- Config file created with restricted ACL from the start (no race condition window)
- Add security reminder to remove cloud-init ISO after first boot (contains passwords)
### Added
- ARM64 architecture detection for Windows on ARM devices
- Log file creation at `$env:TEMP\setup_env_windows_<timestamp>.log`
- Cleanup on failure: automatically removes partial VM, disk, and ISO on error
- Hosts file backup before modification (removed on success, kept on failure)
- Input validation functions: `Test-GitName`, `Test-GitEmail`, `Test-VMPassword`, `Test-VNCPassword`
- Checksum caching to avoid re-downloading verification data
### Changed
- Ubuntu image URL now uses detected architecture instead of hardcoded amd64
- All major operations now log to file for troubleshooting
- VM creation wrapped in try/catch with automatic cleanup on failure
## [0.13.0] - 2025-01-25
### Changed
- Rewrote `setup_env_windows.ps1` to fully implement WINDOWS_PLAN.md
- Password handling uses cloud-init `chpasswd` with plaintext (type: text) instead of broken hash generation
- Multiple ISO creation methods with fallback chain: oscdimg → WSL genisoimage → IMAPI2 COM
- Downloads use BITS transfer for reliability with progress reporting
- SSH readiness checking with timeout before displaying connection info
### Added
- Component skip parameters: `-SkipVNC`, `-SkipPostgreSQL`, `-SkipOllama`, `-SkipPlaywright`
- VNC password support via base64 encoding in cloud-init
- Automatic hosts file cleanup when VM is deleted with `-Force`
- Proper prerequisite checking for Hyper-V, Windows edition, and admin privileges
### Fixed
- Cloud-init password configuration (was using bash syntax in PowerShell)
- ISO creation now works without Windows ADK by using WSL or IMAPI2 fallbacks
- Hosts file handling with proper admin privilege elevation
## [0.12.0] - 2025-01-25
### Added
- Python runtime management via mise (alongside Node.js, Erlang, Elixir)
- `WINDOWS_PLAN.md` documenting Hyper-V implementation strategy and security rationale
### Fixed
- Tidewave CLI download URL (now uses correct `tidewave_app` repo with musl binaries)
### Changed
- Python is now a selectable component managed by mise instead of system apt
## [0.11.0] - 2025-01-25
### Added
- Windows support via Hyper-V for maximum security isolation
- `setup_env_windows.ps1` PowerShell script with full VM provisioning
- Ubuntu cloud image support with cloud-init automation
- SSH key generation for passwordless VM access on Windows
- Hosts file integration for easy `<vmname>.local` access
### Security
- Hyper-V provides stronger isolation than WSL2 (separate kernel, network, filesystem)
- No host integration services enabled by default
## [0.10.0] - 2025-01-25
### Added
- OpenCode: Open-source AI coding assistant with multi-provider support
- Tidewave CLI: Elixir/Phoenix MCP server for AI-powered development
- New component selection options for OpenCode and Tidewave
## [0.9.1] - 2025-01-25
### Fixed
- Add error checking for base64 decode operations in VM provisioning
- Add `set -e` to VM bootstrap script for early failure detection
## [0.9.0] - 2025-01-25
### Security
- Fix rustup pipe-to-shell vulnerability: now downloads to temp file with validation before execution
- Fix SKIP_EXPORTS command injection risk: refactored to use base64-encoded list instead of shell command string
- Fix Playwright symlink path validation: validates executable and path prefix before creating symlinks
## [0.8.0] - 2025-01-25
### Added
- Project memory system using CLAUDE.md, CHANGELOG.md, and README.md
- Versioning rules and documentation update guidelines in CLAUDE.md
## [0.7.0] - 2025-01-25
### Added
- CHANGELOG.md with version history following Keep a Changelog format
## [0.6.0] - 2025-01-25
### Changed
- All tools now use latest versions by default instead of pinning specific versions
- PostgreSQL authentication uses scram-sha-256 for all TCP connections
- Simplified tool installation by removing version pinning constraints
### Security
- VNC passwords are never stored and must be entered each time
- Added documentation for input validation patterns and safe config loading
## [0.5.0] - 2025-01-25
### Security
- Prevents shell injection through input validation and safe parameter passing
- Replaces direct sourcing with manual config parsing to avoid code execution
- Downloads and validates install scripts before execution instead of piping
- Uses base64 encoding for secure VM parameter transmission
- Adds checksum verification for binary downloads
- Creates secure temporary directories and files with proper permissions
## [0.4.0] - 2025-01-25
### Changed
- Replaces sequential installation with parallel step execution
- Introduces real-time progress dashboard with spinner and status
- Removes color variables to improve terminal compatibility
- Restructures logging with per-step files for better debugging
### Performance
- Significantly reduces total setup time by running independent steps concurrently
## [0.3.0] - 2025-01-25
### Added
- Dual-mode operation: orchestration on macOS, provisioning on Linux
- Interactive component selection with visual menu interface
- VNC desktop access for OAuth workflows and browser-based tasks
### Security
- Secure VM creation with disabled host filesystem access
## [0.2.0] - 2025-01-25
### Added
- OrbStack development sandbox setup script
- mise version manager with Node.js, Erlang, and Elixir support
- PostgreSQL 16 with remote access configuration
- Claude Code integration with multiple plugin marketplaces
- Chromium browser and Playwright for automation tasks
## [0.1.0] - 2025-01-25
### Added
- Initial project structure

View file

@ -1,42 +1,17 @@
# CLAUDE.md
## Project Memory
This project uses three documentation files as persistent memory. **You must keep these files up to date when making changes:**
| File | Purpose | Update When |
|------|---------|-------------|
| `CLAUDE.md` | Technical context for Claude Code sessions | Adding new patterns, conventions, or implementation details |
| `CHANGELOG.md` | Version history following Keep a Changelog format | Every commit (add entry, bump version) |
| `README.md` | User-facing documentation | Changing user-visible behavior, adding features, or modifying usage |
### Versioning Rules
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Increment PATCH for bug fixes and minor updates
- Increment MINOR for new features and enhancements
- Increment MAJOR for breaking changes
- Tag every commit with its version: `git tag -a vX.Y.Z -m "Description"`
## Project Overview
This repository provides scripts for creating isolated development sandboxes for Claude Code on macOS (OrbStack) and Windows (Hyper-V). Both platforms offer full VM isolation for safely running AI coding assistants with elevated permissions.
**Supported platforms:**
- **macOS**: OrbStack VMs (ARM64 Apple Silicon)
- **Windows**: Hyper-V VMs (maximum security, stronger than WSL2)
This repository contains a dual-mode setup script for creating OrbStack-based development sandboxes tailored for Claude Code with Elixir/Erlang, browser automation, and PostgreSQL.
## Repository Structure
```
setup_env.sh - macOS script (OrbStack host mode + Linux VM provisioning)
setup_env_windows.ps1 - Windows script (Hyper-V with cloud-init)
WINDOWS_PLAN.md - Windows implementation plan and security rationale
setup_env.sh - Main script (macOS host mode + Linux VM provisioning mode)
config.env.example - Example credentials file
config.env - User credentials (gitignored, created on first run)
.gitignore - Ignores config.env
README.md - User-facing documentation
CHANGELOG.md - Version history (Keep a Changelog format)
CLAUDE.md - This file (context for Claude Code sessions)
```
@ -53,11 +28,11 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end.
- **Target environment**: OrbStack Ubuntu VM on macOS Apple Silicon (ARM64)
- **Version manager**: mise (manages Node.js, Erlang, Elixir)
- **Versions**: All tools use latest by default. Erlang/Elixir versions can be configured at the top of `setup_env.sh` (`ERLANG_VERSION`, `ELIXIR_VERSION`) - set to "latest" or pin to specific versions
- **PostgreSQL auth**: Peer for local socket, scram-sha-256 for all TCP connections (localhost and network)
- **Language versions**: Configured at the top of `setup_env.sh` as variables (`ERLANG_VERSION`, `ELIXIR_VERSION`)
- **PostgreSQL auth**: Peer for local socket, trust for localhost TCP (127.0.0.1), scram-sha-256 for host network (192.168.0.0/16)
- **Browser**: Chromium (no Chrome ARM64 Linux builds exist), symlinked to `google-chrome`
- **VNC**: TigerVNC + XFCE on display :1 (port 5901), controlled via `vnc-start`/`vnc-stop` helpers in `~/bin`
- **Shared credentials**: `config.env` stores git name/email only; VNC password is prompted each time (never stored)
- **Shared credentials**: `config.env` is created once and reused for all VMs
## Working on This Project
@ -86,40 +61,9 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end.
- `--yes`/`-y` accepts all components without prompting but still allows interactive credential entry
- `MISE_GLOBAL_CONFIG_FILE` and `MISE_CONFIG_DIR` are set to prevent OrbStack host-mount config pollution
### Security Patterns (macOS/Linux)
- **Input validation**: Use `validate_vm_name()`, `validate_vnc_password()`, `validate_safe_input()` for all user inputs
- **Safe config loading**: Use `load_config_safely()` which parses key=value pairs without shell execution (never `source`)
- **Credential passing**: Base64-encode values before passing to VM via `orb run` to prevent shell injection
- **Download helper**: Use `download_verified_binary()` for binary downloads (supports optional checksum verification)
- **Temp files**: Always use `mktemp` with restrictive permissions (`chmod 600`/`700`)
- **Symlinks**: Check with `[ ! -e path ]` (not `[ ! -L path ]`) to avoid overwriting existing files
- **Architecture detection**: Use `detect_architecture()` for portable binary downloads
### Editing setup_env_windows.ps1
- PowerShell script for Windows Hyper-V VM creation
- Uses Ubuntu cloud images with cloud-init for automated provisioning
- Supports ARM64 architecture detection for Windows on ARM
- Skip parameters: `-SkipVNC`, `-SkipPostgreSQL`, `-SkipOllama`, `-SkipPlaywright`
- ISO creation fallback chain: oscdimg → WSL genisoimage → IMAPI2 COM
- Log file created at `$env:TEMP\setup_env_windows_<timestamp>.log`
### Security Patterns (Windows)
- **Input validation**: Use `Test-GitName`, `Test-GitEmail`, `Test-VMPassword`, `Test-VNCPassword` for all user inputs
- **Config file security**: ACL set to owner-only before writing content (no race condition)
- **Config validation**: Loaded config.env values are re-validated to detect tampering
- **Checksum verification**: Ubuntu image verified against SHA256 checksums from Canonical
- **Cleanup on failure**: `Invoke-Cleanup` removes partial VM, disk, and ISO on errors
- **Hosts file backup**: Created before modification, removed on success, kept on failure
- **Cloud-init ISO**: Contains passwords in plaintext; user reminded to remove after first boot
### Testing
There are no automated tests. To test changes:
**macOS:**
1. Remove an existing test VM: `orb delete test-sandbox`
2. Run: `./setup_env.sh test-sandbox`
3. Verify provisioning completes
@ -127,21 +71,10 @@ There are no automated tests. To test changes:
5. Test VNC: `ssh test-sandbox@orb -- vnc-start`, then `open vnc://test-sandbox.orb.local:5901`
6. Clean up: `orb delete test-sandbox`
**Windows:**
1. Remove existing test VM: `Stop-VM -Name test-sandbox -Force; Remove-VM -Name test-sandbox -Force`
2. Run: `.\setup_env_windows.ps1 -VMName test-sandbox`
3. Verify provisioning completes (check `/var/log/provision.log` in VM)
4. Test SSH: `ssh -i $env:USERPROFILE\.ssh\id_ed25519_test-sandbox dev@test-sandbox.local`
5. Clean up: `Stop-VM -Name test-sandbox -Force; Remove-VM -Name test-sandbox -Force`
### Security Considerations
- `config.env` is chmod 600 / owner-only ACL and gitignored (stores git name/email only, never passwords)
- PostgreSQL uses scram-sha-256 for all TCP connections (peer auth for local socket)
- `config.env` is chmod 600 and gitignored
- PostgreSQL remote access uses scram-sha-256 (not trust)
- The script refuses to run as root inside the VM
- VNC password is required (min 8 chars), validated to block shell metacharacters, never stored
- VNC binds to all interfaces (`-localhost no`) to allow connections from the host — this is intentional and documented
- All user inputs are validated before use to prevent command injection
- External scripts (mise, ollama) are downloaded to temp files and validated before execution
- Host filesystem access is disabled inside the VM for isolation
- Windows: Ubuntu image downloads are verified against SHA256 checksums
- VNC password is required (min 6 chars)
- VNC binds to all interfaces (`-localhost no`) to allow connections from the macOS host — this is intentional for the OrbStack use case

115
README.md
View file

@ -1,27 +1,20 @@
# Secure AI Coding Sandboxes
# OrbStack Development Sandbox
Disposable, isolated Linux VMs for running Claude Code with `--dangerously-skip-permissions`. One command creates a fully provisioned environment. Blow it away and recreate it in minutes.
The VM is a real Linux machine with its own filesystem, network, and process space — complete isolation from your host with easy access for development.
| Platform | Script | Isolation Level |
|----------|--------|-----------------|
| **macOS** | `setup_env.sh` (OrbStack) | Full VM isolation |
| **Windows** | `setup_env_windows.ps1` (Hyper-V) | Full VM isolation |
The VM is a real Linux machine with its own filesystem, network, and process space — but you edit files from your Mac, access services on `*.orb.local`, and SSH in without any key setup. All the isolation of a container with none of the friction.
## Why This Exists
Running `claude --dangerously-skip-permissions` on your host machine means Claude can execute arbitrary commands, install packages, modify system files, and access everything on your disk. That's powerful for autonomous coding but risky on a machine with your SSH keys, credentials, and personal files.
These scripts create throwaway VMs where Claude can run unrestricted:
This script creates throwaway VMs where Claude can run unrestricted:
- **Isolated filesystem** — Claude can't touch your host files, keys, or configs
- **Isolated filesystem** — Claude can't touch your macOS files, keys, or configs
- **Isolated network** — services run on their own IP, no port conflicts with your host
- **Disposable**delete and recreate in one command
- **Disposable**`orb delete my-sandbox` wipes everything; recreate in one command
- **Multiple VMs** — run separate sandboxes per project with shared git credentials
- **Full access from host** — edit files in your editor, browse databases, view running apps
## Quick Start (macOS)
- **Full access from Mac** — edit files in your editor, browse databases, view running apps
```bash
# Create a sandbox, SSH in, run Claude unrestricted
@ -32,14 +25,14 @@ claude --dangerously-skip-permissions
If anything goes wrong: `orb delete my-project && ./setup_env.sh my-project`
### macOS Setup
## Quick Start
```bash
# Create and provision a VM (one command from macOS)
./setup_env.sh my-sandbox
```
On first run, you'll be prompted for git commit author name and email. These are saved to `config.env` and reused for all future VMs. You'll also get an interactive checklist to select which components to install. If you select VNC, you'll be prompted for a VNC password (this is never stored and must be entered each time).
On first run, you'll be prompted for git name, email, and a VNC password. These are saved to `config.env` and reused for all future VMs. You'll also get an interactive checklist to select which components to install.
```bash
# Create additional VMs — reuses config.env, shows component picker
@ -58,65 +51,11 @@ When run manually inside a VM, you're prompted for each component individually:
./setup_env.sh --yes
```
### macOS Requirements
## Requirements
- macOS with Apple Silicon (ARM64)
- [OrbStack](https://orbstack.dev) installed (`brew install orbstack`)
## Quick Start (Windows)
```powershell
# Run as Administrator
.\setup_env_windows.ps1 -VMName my-project
# Connect via SSH (after provisioning)
ssh -i $env:USERPROFILE\.ssh\id_ed25519_my-project dev@my-project.local
# Run Claude unrestricted
claude --dangerously-skip-permissions
```
If anything goes wrong: `Remove-VM -Name my-project -Force` then run the script again.
### Windows Setup
```powershell
# Create and provision a VM (run as Administrator)
.\setup_env_windows.ps1 -VMName my-sandbox
# Customize resources
.\setup_env_windows.ps1 -VMName my-sandbox -MemoryGB 16 -DiskGB 100 -CPUs 8
# Skip optional components
.\setup_env_windows.ps1 -VMName my-sandbox -SkipVNC -SkipOllama
```
On first run, you'll be prompted for:
- Git commit author name and email (saved to `config.env`)
- VM user password (min 8 chars, not stored)
- VNC password if VNC is enabled (min 8 chars, not stored)
### Windows Requirements
- Windows 10/11 Pro, Enterprise, or Education (Hyper-V not available on Home)
- Hyper-V enabled: `Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All`
- Administrator privileges
- One of: Windows ADK, WSL with genisoimage, or Windows 10+ (IMAPI2 fallback)
### Why Hyper-V (not WSL2)?
WSL2 is convenient but provides weaker isolation:
- Shares kernel with all WSL2 instances
- Host filesystem mounted by default
- Can launch Windows executables from Linux
- Network traffic bypasses Windows firewall
Hyper-V provides **maximum security**:
- Separate kernel per VM
- Complete filesystem isolation
- Own network stack
- No Windows integration by default
## What Gets Installed
All components are optional — deselect what you don't need in the interactive picker.
@ -125,17 +64,13 @@ All components are optional — deselect what you don't need in the interactive
|------|---------|---------|
| mise | latest | Version manager for runtimes |
| Node.js | LTS | JavaScript runtime |
| Erlang | latest | BEAM VM |
| Elixir | latest | Elixir language |
| Erlang | 28.3.1 | BEAM VM |
| Elixir | 1.19.5-otp-28 | Elixir language |
| Chromium | system | Browser automation target |
| Playwright | latest | Browser testing framework |
| PostgreSQL | system default | Database |
| Ollama | latest | Local LLM inference |
| Claude Code | latest | AI coding assistant (Anthropic) |
| OpenCode | latest | Open-source AI coding assistant (multi-provider) |
| Tidewave | latest | Elixir/Phoenix MCP server for AI tools |
| yq | latest | YAML processor |
| watchexec | latest | File watcher (via cargo) |
| Claude Code | latest | AI coding assistant |
| TigerVNC + XFCE | system | VNC access for browser login flows |
## Connecting from macOS
@ -242,7 +177,7 @@ A superuser matching your Linux username and a `dev` database are created automa
**Auth model:**
- Local socket (`psql dev`): peer auth (OS username must match PG role)
- Localhost TCP (127.0.0.1): scram-sha-256 (password required)
- Localhost TCP (127.0.0.1): trust (passwordless)
- Host network (192.168.0.0/16, i.e., from macOS): scram-sha-256
### From inside the VM
@ -311,18 +246,16 @@ The script installs these plugins at user scope:
## Configuration
All tools are configured to use the latest versions by default. Erlang and Elixir versions can be customized in `setup_env.sh`:
Edit the version variables at the top of `setup_env.sh`:
```bash
ERLANG_VERSION="latest" # or pin to specific version like "28.3.1"
ELIXIR_VERSION="latest" # or pin to specific version like "1.19.5-otp-28"
ERLANG_VERSION="28.3.1"
ELIXIR_VERSION="1.19.5-otp-28"
```
### Shared credentials
Git credentials (name, email) are stored in `config.env` (gitignored). VNC passwords are never stored and must be entered each time you create a VM with VNC enabled.
To reset git credentials:
Credentials are stored in `config.env` (gitignored). To reset:
```bash
rm config.env
@ -361,16 +294,4 @@ The VM provisioning script is safe to run multiple times. It checks for existing
## Logs
Each provisioning run creates a log file at `/tmp/setup_env_<timestamp>.log` inside the VM with detailed output from package installations and any errors. Log files are created with mode 600 (owner-only access).
## Security
The script includes several security hardening measures:
- **Input validation**: VM names, VNC passwords, and git credentials are validated against strict patterns
- **No credential storage**: VNC passwords are prompted each time and never written to disk
- **Safe credential passing**: Values are base64-encoded when passed to the VM to prevent shell injection
- **Config file security**: `config.env` is created with mode 600 and parsed safely (not sourced)
- **Secure temp files**: Uses `mktemp` with restrictive permissions for all temporary files
- **Host filesystem isolation**: macOS home directory access is disabled inside the VM
- **PostgreSQL hardening**: Uses peer auth for local sockets, scram-sha-256 for network connections
Each provisioning run creates a log file at `/tmp/setup_env_<timestamp>.log` inside the VM with detailed output from package installations and any errors.

View file

@ -1,257 +0,0 @@
# Windows Hyper-V Implementation Plan
This document describes the plan for implementing secure, isolated development sandboxes on Windows using Hyper-V instead of WSL2.
## Why Hyper-V Over WSL2
### WSL2 Security Limitations
| Issue | Impact |
|-------|--------|
| **Shared kernel** | All WSL2 instances share Microsoft's Linux kernel |
| **Host filesystem access** | `/mnt/c` mounted by default, can access Windows files |
| **Bidirectional execution** | Linux can launch Windows executables |
| **Firewall bypass** | WSL2 outbound traffic bypasses Windows firewall rules |
| **Process visibility** | Windows has limited visibility into WSL2 processes |
| **Credential exposure** | Linux processes can potentially access Windows credentials |
### CVE History
- **CVE-2024-20681**: Local privilege escalation to SYSTEM via WSL
- **CVE-2025-9074**: Container escape via WSL2 filesystem sharing
- **CVE-2025-53788**: Undisclosed flaw in WSL/host communication
### Hyper-V Isolation Model
| Feature | Benefit |
|---------|---------|
| **Separate kernel** | Each VM runs its own Linux kernel |
| **Complete filesystem isolation** | No access to Windows files by default |
| **Own network stack** | Separate IP, no firewall bypass |
| **No Windows integration** | Cannot launch Windows programs |
| **Snapshot support** | Can checkpoint and restore VM state |
| **Hardware isolation** | Uses VT-x/AMD-V virtualization |
## Implementation Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Windows Host │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ setup_env_windows.ps1│ │ Hyper-V Manager │ │
│ │ │ │ │ │
│ │ • Check prereqs │───▶│ ┌──────────────────────────┐ │ │
│ │ • Download image │ │ │ Ubuntu VM │ │ │
│ │ • Create cloud-init │ │ │ │ │ │
│ │ • Provision VM │ │ │ • Isolated filesystem │ │ │
│ │ • Configure network │ │ │ • Own network (NAT) │ │ │
│ └─────────────────────┘ │ │ • Claude Code + tools │ │ │
│ │ │ • No Windows access │ │ │
│ │ └──────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Script Workflow
### Phase 1: Prerequisites Check
```powershell
# Verify environment
1. Check Administrator privileges
2. Check Windows edition (Pro/Enterprise/Education required)
3. Check Hyper-V enabled
4. Check for existing VM (handle -Force flag)
5. Find suitable network switch
```
### Phase 2: Configuration
```powershell
# Collect credentials
1. Load config.env if exists (git name/email)
2. Prompt for missing values
3. Prompt for VM password (not stored)
4. Generate SSH key pair for VM access
```
### Phase 3: Image Preparation
```powershell
# Prepare Ubuntu cloud image
1. Download Ubuntu cloud image (VHDX format)
2. Cache in $env:ProgramData\HyperV-DevSandbox\Images
3. Copy and resize for new VM
4. Generate cloud-init ISO with:
- User account configuration
- SSH key injection
- Package installation
- Setup script embedding
```
### Phase 4: VM Creation
```powershell
# Create Hyper-V VM
1. Create Generation 2 VM (UEFI)
2. Configure resources (CPU, RAM, disk)
3. Disable Secure Boot (for Ubuntu)
4. Attach cloud-init ISO
5. Disable integration services for isolation
6. Enable nested virtualization (for Docker)
```
### Phase 5: Provisioning
```powershell
# Start and provision
1. Start VM
2. Wait for IP assignment
3. Add to hosts file (<vmname>.local)
4. Cloud-init runs setup_env.sh internally
5. Display connection instructions
```
## Cloud-Init Configuration
```yaml
#cloud-config
hostname: <vmname>
users:
- name: dev
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- <generated-public-key>
packages:
- openssh-server
- curl
- git
write_files:
- path: /tmp/setup_env.sh
content: <base64-encoded-script>
runcmd:
- bash /tmp/setup_env.sh --non-interactive --yes
```
## Security Hardening
### VM Configuration
```powershell
# Disable integration services for maximum isolation
Disable-VMIntegrationService -Name "Guest Service Interface"
Disable-VMIntegrationService -Name "Heartbeat"
# Keep Time Synchronization and Shutdown for usability
```
### Network Isolation Options
1. **NAT (Default)**: VM can access internet, isolated from LAN
2. **Internal Only**: VM can only communicate with host
3. **Private**: Completely isolated network
### Firewall Rules (Optional)
```powershell
# Allow SSH access only
New-NetFirewallRule -Name "HyperV-SSH-$VMName" `
-Direction Inbound -LocalPort 22 -Protocol TCP `
-RemoteAddress <VM-IP> -Action Allow
```
## Requirements
### Windows Edition
- Windows 10/11 Pro
- Windows 10/11 Enterprise
- Windows 10/11 Education
- Windows Server 2016+
**Not supported**: Windows 10/11 Home (no Hyper-V)
### Hardware
- 64-bit processor with SLAT (Second Level Address Translation)
- VM Monitor Mode extensions (VT-x on Intel, AMD-V on AMD)
- Minimum 4 GB RAM (8+ GB recommended)
- BIOS/UEFI virtualization enabled
### Software
- Hyper-V enabled: `Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All`
- Windows ADK (for cloud-init ISO creation): [Download](https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install)
## Usage
### Create Sandbox
```powershell
# Basic usage (run as Administrator)
.\setup_env_windows.ps1 -VMName my-project
# With custom resources
.\setup_env_windows.ps1 -VMName my-project -MemoryGB 16 -DiskGB 100 -CPUs 8
# Replace existing VM
.\setup_env_windows.ps1 -VMName my-project -Force
```
### Connect to Sandbox
```powershell
# Via SSH (recommended)
ssh -i $env:USERPROFILE\.ssh\id_ed25519_<vmname> dev@<vmname>.local
# Via Hyper-V console
vmconnect localhost <vmname>
```
### Manage Sandbox
```powershell
# Stop VM
Stop-VM -Name <vmname>
# Start VM
Start-VM -Name <vmname>
# Get VM IP
(Get-VM -Name <vmname> | Get-VMNetworkAdapter).IPAddresses
# Delete VM completely
Stop-VM -Name <vmname> -Force
Remove-VM -Name <vmname> -Force
Remove-Item -Path "$env:ProgramData\HyperV-DevSandbox\VMs\<vmname>" -Recurse -Force
```
## Comparison: OrbStack vs Hyper-V
| Feature | OrbStack (macOS) | Hyper-V (Windows) |
|---------|------------------|-------------------|
| Host OS | macOS only | Windows only |
| Setup complexity | Low | Medium |
| Isolation | Full VM | Full VM |
| DNS | `*.orb.local` auto | Manual hosts file |
| SSH | Automatic | Key-based |
| Filesystem sharing | Configurable | Disabled by default |
| Performance | Near-native | Near-native |
| Nested virtualization | Yes | Yes |
## Known Limitations
1. **Windows ADK Required**: Cloud-init ISO creation requires `oscdimg` from Windows ADK
2. **Manual DNS**: No automatic DNS like OrbStack's `*.orb.local`
3. **Key Management**: SSH keys generated per-VM, stored in user's .ssh folder
4. **No Clipboard Sharing**: Disabled for security (can be enabled if needed)
5. **Generation 2 Only**: Uses UEFI, no legacy BIOS support
## Future Improvements
- [ ] Add option for WSL2 (for users who prefer convenience over security)
- [ ] Implement automatic DNS via Windows hosts file or local DNS server
- [ ] Add PowerShell ISO creation without Windows ADK dependency
- [ ] Support for VM templates/checkpoints
- [ ] Integration with Windows Terminal for easy access
- [ ] Optional WinRM configuration for PowerShell remoting

View file

@ -25,11 +25,10 @@
set -euo pipefail
# ============================================================================
# Configuration
# Configuration - edit versions here
# ============================================================================
# Use 'latest' for mise-managed tools to always get the newest version
ERLANG_VERSION="latest"
ELIXIR_VERSION="latest"
ERLANG_VERSION="28.3.1"
ELIXIR_VERSION="1.19.5-otp-28"
# ============================================================================
# Helpers
@ -64,113 +63,6 @@ command_exists() {
command -v "$1" &>/dev/null
}
# Security: Validate input contains only safe characters for config storage
# Allows alphanumeric, spaces, common punctuation, but blocks shell metacharacters
validate_safe_input() {
local input="$1"
local field_name="$2"
# Block dangerous shell metacharacters: backticks, $, (), {}, ;, |, &, <, >, newlines
if [[ "$input" =~ [\`\$\(\)\{\}\;\|\&\<\>\"] ]] || [[ "$input" == *$'\n'* ]]; then
log_error "$field_name contains invalid characters (shell metacharacters not allowed)"
return 1
fi
return 0
}
# Security: Validate VM name contains only safe characters
validate_vm_name() {
local name="$1"
if [[ ! "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
log_error "VM name must start with a letter and contain only alphanumeric characters, hyphens, and underscores"
return 1
fi
if [ ${#name} -gt 64 ]; then
log_error "VM name must be 64 characters or less"
return 1
fi
return 0
}
# Security: Validate VNC password
validate_vnc_password() {
local password="$1"
if [ ${#password} -lt 6 ]; then
echo "Password must be at least 6 characters."
return 1
fi
if [ ${#password} -gt 64 ]; then
echo "Password must be 64 characters or less."
return 1
fi
# Block characters that could cause shell injection
if [[ "$password" =~ [\`\$\(\)\{\}\;\|\&\<\>\'\"] ]]; then
echo "Password contains invalid characters."
return 1
fi
return 0
}
# Security: Detect system architecture and return normalized name
# Returns: amd64, arm64, or exits with error for unsupported architectures
detect_architecture() {
local arch
arch=$(uname -m)
case "$arch" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
*)
log_error "Unsupported architecture: $arch"
return 1
;;
esac
}
# Security: Download and verify a binary with optional checksum
# Args: $1=url, $2=output_path, $3=expected_checksum (optional, sha256)
download_verified_binary() {
local url="$1"
local output="$2"
local expected_checksum="${3:-}"
local temp_file
temp_file=$(mktemp)
# Download to temp file first
if ! curl -fsSL "$url" -o "$temp_file"; then
log_error "Failed to download: $url"
rm -f "$temp_file"
return 1
fi
# Verify checksum if provided
if [ -n "$expected_checksum" ]; then
local actual_checksum
actual_checksum=$(sha256sum "$temp_file" | awk '{print $1}')
if [ "$actual_checksum" != "$expected_checksum" ]; then
log_error "Checksum mismatch for $url"
log_error "Expected: $expected_checksum"
log_error "Got: $actual_checksum"
rm -f "$temp_file"
return 1
fi
fi
# Move to final location
if [[ "$output" == /usr/* ]]; then
sudo mv "$temp_file" "$output"
sudo chmod +x "$output"
else
mv "$temp_file" "$output"
chmod +x "$output"
fi
return 0
}
# ============================================================================
# macOS Host Mode
# ============================================================================
@ -195,87 +87,42 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
# VM name from argument or default
VM_NAME="${1:-dev-sandbox}"
# Security: Validate VM name (Issue #6)
if ! validate_vm_name "$VM_NAME"; then
exit 1
fi
# Locate config.env relative to this script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.env"
# Security: Load config safely without executing code (Issue #2)
# Instead of sourcing, parse key=value pairs manually
load_config_safely() {
local config_file="$1"
while IFS='=' read -r key value || [ -n "$key" ]; do
# Skip empty lines and comments
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
# Remove leading/trailing whitespace from key
key=$(echo "$key" | xargs)
# Remove surrounding quotes from value
value=$(echo "$value" | sed 's/^"//;s/"$//;s/^'"'"'//;s/'"'"'$//')
case "$key" in
GIT_NAME) GIT_NAME="$value" ;;
GIT_EMAIL) GIT_EMAIL="$value" ;;
# VNC_PASSWORD intentionally not stored - prompted each time for security
esac
done < "$config_file"
}
# Create or load config
if [ -f "$CONFIG_FILE" ]; then
load_config_safely "$CONFIG_FILE"
source "$CONFIG_FILE"
echo -e "${YELLOW}Using saved config from: $CONFIG_FILE${NC}"
echo " Name: $GIT_NAME"
echo " Email: $GIT_EMAIL"
echo " VNC Pass: ******"
echo ""
else
echo "First run — creating config file for shared credentials."
echo ""
# Security: Validate git name (Issue #5, #10, #11)
read -rp "Enter your name for git config: " GIT_NAME
read -rp "Enter your email for git config: " GIT_EMAIL
while true; do
read -rp "Git commit author name: " GIT_NAME
if [ -z "$GIT_NAME" ]; then
echo "Name is required."
continue
fi
if [ ${#GIT_NAME} -gt 256 ]; then
echo "Name must be 256 characters or less."
continue
fi
if validate_safe_input "$GIT_NAME" "Name"; then
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
echo
if [ ${#VNC_PASSWORD} -ge 6 ]; then
break
fi
echo "Password must be at least 6 characters."
done
# Security: Validate git email (Issue #5, #10, #11)
while true; do
read -rp "Git commit author email: " GIT_EMAIL
if [ -z "$GIT_EMAIL" ]; then
echo "Email is required."
continue
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
log_error "Name and email are required."
exit 1
fi
if [ ${#GIT_EMAIL} -gt 256 ]; then
echo "Email must be 256 characters or less."
continue
fi
if ! [[ "$GIT_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
echo "Please enter a valid email address."
continue
fi
if validate_safe_input "$GIT_EMAIL" "Email"; then
break
fi
done
# Security: Write config with proper escaping (Issue #10)
# VNC password is NOT stored - will be prompted when VNC is selected
{
printf 'GIT_NAME="%s"\n' "$GIT_NAME"
printf 'GIT_EMAIL="%s"\n' "$GIT_EMAIL"
} > "$CONFIG_FILE"
cat > "$CONFIG_FILE" <<EOF
GIT_NAME="$GIT_NAME"
GIT_EMAIL="$GIT_EMAIL"
VNC_PASSWORD="$VNC_PASSWORD"
EOF
chmod 600 "$CONFIG_FILE"
echo ""
echo "Config saved to: $CONFIG_FILE"
@ -284,34 +131,30 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
# ---- Interactive component selection ----
# Component IDs and descriptions
COMP_IDS=(postgresql mise node python erlang chromium vnc ollama claude opencode playwright plugins)
COMP_IDS=(postgresql mise node erlang chromium vnc ollama claude playwright plugins)
COMP_NAMES=(
"PostgreSQL"
"mise"
"Node.js (LTS)"
"Python (latest)"
"Erlang/Elixir"
"Chromium"
"VNC + XFCE"
"Ollama"
"Claude Code"
"OpenCode"
"Playwright"
"Claude Plugins + Tidewave"
"Claude Plugins"
)
COMP_DESCS=(
"Database server for local development"
"Version manager for Node.js, Python, Erlang, Elixir"
"JavaScript runtime (required for Claude/OpenCode)"
"Python runtime managed by mise"
"Version manager for Node.js, Erlang, Elixir"
"JavaScript runtime (required for Claude Code)"
"BEAM VM + Elixir functional language"
"Browser for automation and testing"
"Remote desktop for browser-based login flows"
"Local LLM runner for offline inference"
"AI coding assistant CLI from Anthropic"
"Open-source AI coding assistant (multi-provider)"
"Browser testing and automation framework"
"Code review, Tidewave MCP, browser automation"
"Code review, feature dev, browser automation plugins"
)
# All selected by default
@ -394,12 +237,12 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
echo ""
# Build list of skipped component IDs (space-separated, safe characters only)
# Security: Only hardcoded component IDs from COMP_IDS are used, no user input
SKIP_LIST=""
# Build SKIP env var exports for unselected components
SKIP_EXPORTS=""
for ((i=0; i<COMP_COUNT; i++)); do
if [ "${COMP_SELECTED[$i]}" = "0" ]; then
SKIP_LIST="${SKIP_LIST}${COMP_IDS[$i]} "
UPPER_ID=$(echo "${COMP_IDS[$i]}" | tr '[:lower:]' '[:upper:]')
SKIP_EXPORTS="${SKIP_EXPORTS}export SKIP_${UPPER_ID}=1; "
fi
done
@ -417,27 +260,6 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
fi
echo ""
# Security: Prompt for VNC password only when VNC is selected (Issue #4)
# Password is never stored to disk - prompted fresh each time
VNC_PASSWORD=""
VNC_INDEX=-1
for ((i=0; i<COMP_COUNT; i++)); do
if [ "${COMP_IDS[$i]}" = "vnc" ]; then
VNC_INDEX=$i
break
fi
done
if [ "$VNC_INDEX" -ge 0 ] && [ "${COMP_SELECTED[$VNC_INDEX]}" = "1" ]; then
while true; do
read -rsp "Enter VNC password (6-64 chars): " VNC_PASSWORD
echo
if validate_vnc_password "$VNC_PASSWORD"; then
break
fi
done
echo ""
fi
# Check if VM already exists
if orb list 2>/dev/null | grep -qw "$VM_NAME"; then
log_error "VM '$VM_NAME' already exists."
@ -467,32 +289,12 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
orb push -m "$VM_NAME" "$SCRIPT_DIR/setup_env.sh" /tmp/setup_env.sh
echo ""
if [ -n "$SKIP_LIST" ]; then
echo -e "${YELLOW}Skipping:${NC} $SKIP_LIST"
if [ -n "$SKIP_EXPORTS" ]; then
echo -e "${YELLOW}Skipping:${NC} $SKIP_EXPORTS"
fi
echo -e "${YELLOW}Running provisioning inside VM (this will take a while)...${NC}"
echo ""
# Security: Use base64 encoding to safely pass all values to VM
# This prevents any shell injection through special characters
GIT_NAME_B64=$(printf '%s' "$GIT_NAME" | base64)
GIT_EMAIL_B64=$(printf '%s' "$GIT_EMAIL" | base64)
VNC_PASSWORD_B64=$(printf '%s' "$VNC_PASSWORD" | base64)
SKIP_LIST_B64=$(printf '%s' "$SKIP_LIST" | base64)
# Security: All user-controlled values are base64-encoded before passing to VM
# The decode script sets SKIP_* env vars from the safe SKIP_LIST
orb run -m "$VM_NAME" bash -c "
set -e
export GIT_NAME=\$(echo '$GIT_NAME_B64' | base64 -d) || { echo 'Failed to decode GIT_NAME'; exit 1; }
export GIT_EMAIL=\$(echo '$GIT_EMAIL_B64' | base64 -d) || { echo 'Failed to decode GIT_EMAIL'; exit 1; }
export VNC_PASSWORD=\$(echo '$VNC_PASSWORD_B64' | base64 -d) || { echo 'Failed to decode VNC_PASSWORD'; exit 1; }
for comp in \$(echo '$SKIP_LIST_B64' | base64 -d); do
upper=\$(echo \"\$comp\" | tr '[:lower:]' '[:upper:]')
export \"SKIP_\${upper}=1\"
done
bash /tmp/setup_env.sh --non-interactive
"
orb run -m "$VM_NAME" bash -c "${SKIP_EXPORTS}export GIT_NAME='$GIT_NAME'; export GIT_EMAIL='$GIT_EMAIL'; export VNC_PASSWORD='$VNC_PASSWORD'; bash /tmp/setup_env.sh --non-interactive"
echo ""
echo -e "${GREEN}============================================================================${NC}"
@ -524,18 +326,9 @@ fi
# Linux VM Mode (everything below runs inside the VM)
# ============================================================================
# Security: Create log file with restrictive permissions (Issue #13)
LOG_FILE="/tmp/setup_env_$(date +%Y%m%d_%H%M%S).log"
touch "$LOG_FILE"
chmod 600 "$LOG_FILE"
# Security: Use mktemp for unpredictable temp directory (Issue #8, #9)
LOG_DIR=$(mktemp -d "/tmp/setup_env_steps.XXXXXXXXXX")
if [ ! -d "$LOG_DIR" ]; then
log_error "Failed to create secure temp directory"
exit 1
fi
chmod 700 "$LOG_DIR"
LOG_DIR="/tmp/setup_env_steps_$$"
mkdir -p "$LOG_DIR"
# Prevent mise from reading host config via OrbStack mount (/Users/john/.mise.toml)
export MISE_GLOBAL_CONFIG_FILE="$HOME/.config/mise/config.toml"
@ -836,7 +629,7 @@ cleanup() {
fi
}
# Note: trap is set later to include both cleanup and stop_sudo_keepalive
trap cleanup EXIT
# ============================================================================
# Step functions (each is a self-contained install)
@ -865,12 +658,7 @@ install_postgresql() {
[ -z "$PG_HBA" ] || [ -z "$PG_CONF" ] && return 1
# Security: Use 'peer' for local connections (authenticates via OS user)
# This is more secure than 'trust' while still convenient for development
# Users connect via: psql -U $USER dbname (no password needed if OS user matches DB user)
sudo sed -i 's/^local\s\+all\s\+all\s\+peer$/local all all peer/' "$PG_HBA"
# For TCP localhost, keep scram-sha-256 (requires password) instead of trust
# This prevents any local process from accessing the DB without credentials
sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+scram-sha-256$/host all all 127.0.0.1\/32 trust/' "$PG_HBA"
if ! sudo grep -q "# OrbStack host access" "$PG_HBA"; then
echo "# OrbStack host access" | sudo tee -a "$PG_HBA" > /dev/null
@ -890,30 +678,7 @@ install_mise() {
echo "mise already installed"
return 0
fi
# Security: Download script first, validate, then execute (Issue #3)
# This allows for inspection and avoids direct pipe to shell
local mise_script
mise_script=$(mktemp)
curl -fsSL https://mise.run -o "$mise_script"
# Verify it's a shell script with multiple checks
if ! head -1 "$mise_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
log_error "Downloaded mise script doesn't have a valid shell shebang"
rm -f "$mise_script"
return 1
fi
# Check file size is reasonable (not empty, not huge)
local script_size
script_size=$(wc -c < "$mise_script")
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
log_error "Downloaded mise script has suspicious size: $script_size bytes"
rm -f "$mise_script"
return 1
fi
sh "$mise_script"
rm -f "$mise_script"
curl -fsSL https://mise.run | sh
# Add to bashrc
if ! grep -q 'mise activate bash' ~/.bashrc; then
@ -933,15 +698,6 @@ install_node() {
export PATH="$HOME/.local/share/mise/shims:$PATH"
}
install_python() {
export PATH="$HOME/.local/bin:$PATH"
eval "$(~/.local/bin/mise activate bash)"
mise use -g python@latest
export PATH="$HOME/.local/share/mise/shims:$PATH"
# Ensure pip is available via mise python
python -m pip install --upgrade pip
}
install_erlang() {
export PATH="$HOME/.local/bin:$PATH"
eval "$(~/.local/bin/mise activate bash)"
@ -970,11 +726,11 @@ install_chromium() {
sudo apt-get install -y -q chromium --no-install-recommends 2>/dev/null || true
fi
# Symlink if installed (check for existing file, not just symlink)
# Symlink if installed
local bin=""
command_exists chromium-browser && bin=$(which chromium-browser)
command_exists chromium && bin=$(which chromium)
if [ -n "$bin" ] && [ ! -e /usr/bin/google-chrome ]; then
if [ -n "$bin" ] && [ ! -L /usr/bin/google-chrome ]; then
sudo ln -sf "$bin" /usr/bin/google-chrome
fi
}
@ -982,17 +738,8 @@ install_chromium() {
install_vnc() {
sudo apt-get install -y -q tigervnc-standalone-server xfce4 xfce4-terminal dbus-x11
# Create .vnc directory with secure permissions
mkdir -p ~/.vnc
chmod 700 ~/.vnc
# Security: Create password file atomically with correct permissions
# Use umask to ensure file is created with 600 permissions from the start
(
umask 077
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
)
# Ensure permissions are correct (belt and suspenders)
chmod 600 ~/.vnc/passwd
cat > ~/.vnc/xstartup <<'XEOF'
@ -1007,10 +754,6 @@ XEOF
cat > ~/bin/vnc-start <<'VEOF'
#!/bin/bash
# Security note: VNC listens on all interfaces (-localhost no) to allow
# connections from the macOS host via OrbStack's network. The VM is only
# accessible from the host machine, not external networks.
# VNC is password-protected (set during setup).
VNC_HOST="$(hostname -s).orb.local"
if pgrep -f "Xtigervnc :1" > /dev/null 2>&1; then
echo "VNC is already running on display :1"
@ -1039,29 +782,7 @@ install_ollama() {
echo "Ollama already installed"
return 0
fi
# Security: Download script first, validate, then execute with sudo (Issue #3)
local ollama_script
ollama_script=$(mktemp)
curl -fsSL https://ollama.com/install.sh -o "$ollama_script"
# Verify it's a shell script with multiple checks
if ! head -1 "$ollama_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
log_error "Downloaded Ollama script doesn't have a valid shell shebang"
rm -f "$ollama_script"
return 1
fi
# Check file size is reasonable (not empty, not huge)
local script_size
script_size=$(wc -c < "$ollama_script")
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
log_error "Downloaded Ollama script has suspicious size: $script_size bytes"
rm -f "$ollama_script"
return 1
fi
sudo sh "$ollama_script"
rm -f "$ollama_script"
curl -fsSL https://ollama.com/install.sh | sudo sh
}
install_claude() {
@ -1071,78 +792,19 @@ install_claude() {
install_playwright() {
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
npm install -g playwright
playwright install --with-deps chromium
npx --yes playwright install --with-deps chromium
# Symlink Playwright's Chromium if no system one (check for existing files)
# Symlink Playwright's Chromium if no system one
if ! command_exists chromium-browser && ! command_exists chromium; then
local pw_chrome
pw_chrome=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1)
# Security: Validate the found path before creating symlinks
# - Must not be empty
# - Must be an executable file
# - Must be within the expected playwright cache directory
if [ -n "$pw_chrome" ] && [ -x "$pw_chrome" ] && [[ "$pw_chrome" == "$HOME/.cache/ms-playwright"* ]]; then
[ ! -e /usr/local/bin/chromium ] && sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
[ ! -e /usr/local/bin/google-chrome ] && sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
local pw_chrome=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1)
if [ -n "$pw_chrome" ]; then
sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
echo "Using Playwright's bundled Chromium"
fi
fi
}
install_opencode() {
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
if command_exists opencode; then
echo "OpenCode already installed"
return 0
fi
npm install -g opencode-ai
}
install_tidewave() {
if command_exists tidewave; then
echo "Tidewave already installed"
return 0
fi
# Download Tidewave CLI binary from tidewave_app releases
# Binary naming: tidewave-cli-<arch>-unknown-linux-musl
local machine_arch
machine_arch=$(uname -m)
local tidewave_arch=""
case "$machine_arch" in
x86_64) tidewave_arch="x86_64" ;;
aarch64) tidewave_arch="aarch64" ;;
*)
log_error "Unsupported architecture for Tidewave: $machine_arch"
return 1
;;
esac
# Get latest release URL from GitHub (tidewave_app repo)
local release_url="https://github.com/tidewave-ai/tidewave_app/releases/latest/download/tidewave-cli-${tidewave_arch}-unknown-linux-musl"
# Security: Download to temp file first, validate, then install
local temp_file
temp_file=$(mktemp)
if curl -fsSL "$release_url" -o "$temp_file"; then
# Verify it's an executable (ELF binary)
if file "$temp_file" | grep -q "ELF"; then
sudo mv "$temp_file" /usr/local/bin/tidewave
sudo chmod +x /usr/local/bin/tidewave
echo "Tidewave CLI installed"
else
log_error "Downloaded tidewave is not a valid binary"
rm -f "$temp_file"
return 1
fi
else
log_error "Failed to download Tidewave CLI"
rm -f "$temp_file"
return 1
fi
}
install_plugins() {
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
mkdir -p ~/.config/claude
@ -1162,12 +824,6 @@ install_plugins() {
echo "Adding MCP servers..."
claude mcp add playwright --scope user -- npx @anthropic-ai/mcp-server-playwright 2>/dev/null || true
claude mcp add superpowers-chrome --scope user -- npx github:obra/superpowers-chrome --headless 2>/dev/null || true
# Note: Tidewave MCP is configured per-project when running a Phoenix app
# Use: claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp
echo ""
echo "Tidewave MCP: Add per-project with your Phoenix app running:"
echo " claude mcp add --transport http tidewave http://localhost:PORT/tidewave/mcp"
}
install_base_deps() {
@ -1182,55 +838,20 @@ install_base_deps() {
python3 python3-pip python3-venv
# fd symlink (fd-find installs as 'fdfind', symlink to 'fd')
# Check for existing file, not just symlink
if [ ! -e /usr/local/bin/fd ] && command_exists fdfind; then
if [ ! -L /usr/local/bin/fd ] && command_exists fdfind; then
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
fi
# yq - with architecture detection (always latest)
# yq
if ! command_exists yq; then
local yq_arch
local arch
arch=$(detect_architecture)
case "$arch" in
amd64) yq_arch="amd64" ;;
arm64) yq_arch="arm64" ;;
esac
local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${yq_arch}"
download_verified_binary "$yq_url" "/usr/local/bin/yq"
sudo curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" -o /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
fi
# watchexec - install via cargo (always latest)
# watchexec
if ! command_exists watchexec; then
if command_exists cargo; then
cargo install watchexec-cli
else
# Security: Download rustup script first, validate, then execute (Issue #3)
# Same pattern as mise/ollama - never pipe directly to shell
local rustup_script
rustup_script=$(mktemp)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o "$rustup_script"
# Verify it's a shell script
if ! head -1 "$rustup_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
log_error "Downloaded rustup script doesn't have a valid shell shebang"
rm -f "$rustup_script"
return 1
fi
# Check file size is reasonable (not empty, not huge)
local script_size
script_size=$(wc -c < "$rustup_script")
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 2000000 ]; then
log_error "Downloaded rustup script has suspicious size: $script_size bytes"
rm -f "$rustup_script"
return 1
fi
sh "$rustup_script" -y
rm -f "$rustup_script"
source "$HOME/.cargo/env"
cargo install watchexec-cli
fi
curl -fsSL "https://github.com/watchexec/watchexec/releases/download/v2.3.2/watchexec-2.3.2-aarch64-unknown-linux-gnu.tar.xz" | \
sudo tar -xJ --strip-components=1 -C /usr/local/bin/ --wildcards '*/watchexec'
fi
# direnv hook
@ -1254,55 +875,35 @@ if ! command_exists sudo; then
exit 1
fi
# Security: Sudo keepalive - prevent sudo timeout during long operations
# Runs in background and refreshes sudo timestamp every 50 seconds
SUDO_KEEPALIVE_PID=""
start_sudo_keepalive() {
# In non-interactive mode (VM), sudo should be passwordless - skip keepalive
if [ "$NON_INTERACTIVE" = true ]; then
return 0
fi
# Validate sudo access first (allow password prompt to show)
if ! sudo -v; then
log_error "Failed to obtain sudo privileges"
return 1
fi
# Start background process to refresh sudo timestamp
(
while true; do
sudo -n true 2>/dev/null
sleep 50
done
) &
SUDO_KEEPALIVE_PID=$!
}
stop_sudo_keepalive() {
if [ -n "$SUDO_KEEPALIVE_PID" ] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
wait "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
fi
}
# Ensure keepalive is stopped on exit
# Update trap to include both cleanup functions
trap 'stop_sudo_keepalive; cleanup' EXIT
# ============================================================================
# Git configuration prompts (VM-side, non-interactive mode only)
# Git configuration prompts
# ============================================================================
if [ "$NON_INTERACTIVE" = true ]; then
if [ -z "${GIT_NAME:-}" ] || [ -z "${GIT_EMAIL:-}" ]; then
log_error "In non-interactive mode, GIT_NAME and GIT_EMAIL env vars are required."
exit 1
fi
# Only require VNC_PASSWORD if VNC is being installed
if [ -z "${SKIP_VNC:-}" ] && [ -z "${VNC_PASSWORD:-}" ]; then
log_error "In non-interactive mode, VNC_PASSWORD env var is required when VNC is selected (min 6 chars)."
if [ -z "${VNC_PASSWORD:-}" ]; then
log_error "In non-interactive mode, VNC_PASSWORD env var is required (min 6 chars)."
exit 1
fi
else
read -rp "Enter your name for git config: " GIT_NAME
read -rp "Enter your email for git config: " GIT_EMAIL
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
log_error "Name and email are required."
exit 1
fi
while true; do
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
echo
if [ ${#VNC_PASSWORD} -ge 6 ]; then
break
fi
echo "Password must be at least 6 characters."
done
fi
# ============================================================================
@ -1312,15 +913,12 @@ register_step "base" "System packages (git, build tools, curl)"
register_step "postgresql" "PostgreSQL database server"
register_step "mise" "mise version manager"
register_step "node" "Node.js LTS runtime"
register_step "python" "Python (mise-managed)"
register_step "erlang" "Erlang/OTP VM"
register_step "elixir" "Elixir language"
register_step "chromium" "Chromium browser"
register_step "vnc" "VNC server + XFCE desktop"
register_step "ollama" "Ollama local LLM runner"
register_step "claude" "Claude Code CLI"
register_step "opencode" "OpenCode CLI"
register_step "tidewave" "Tidewave CLI (Elixir MCP)"
register_step "playwright" "Playwright browser automation"
register_step "plugins" "Claude Code plugins"
@ -1328,29 +926,21 @@ register_step "plugins" "Claude Code plugins"
should_skip "postgresql" && skip_step "postgresql"
should_skip "mise" && skip_step "mise"
should_skip "node" && skip_step "node"
should_skip "python" && skip_step "python"
should_skip "erlang" && skip_step "erlang"
should_skip "chromium" && skip_step "chromium"
should_skip "vnc" && skip_step "vnc"
should_skip "ollama" && skip_step "ollama"
should_skip "claude" && skip_step "claude"
should_skip "opencode" && skip_step "opencode"
should_skip "playwright" && skip_step "playwright"
should_skip "plugins" && skip_step "plugins"
# Elixir skipped if Erlang skipped
[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "elixir"
# Tidewave skipped if Erlang skipped (it's for Phoenix/Elixir projects)
[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "tidewave"
# Python skipped if mise skipped
[ "${STEP_STATUS[mise]}" = "skipped" ] && skip_step "python"
# Claude/OpenCode/Playwright skipped if Node skipped
# Claude/Playwright skipped if Node skipped
if [ "${STEP_STATUS[node]}" = "skipped" ]; then
[ "${STEP_STATUS[mise]}" = "skipped" ] || true # mise can still install
skip_step "claude"
skip_step "opencode"
skip_step "playwright"
skip_step "plugins"
fi
@ -1366,11 +956,6 @@ echo ""
draw_dashboard
echo ""
# ============================================================================
# Start sudo keepalive to prevent timeout during long operations
# ============================================================================
start_sudo_keepalive
# ============================================================================
# Phase 1: Base dependencies (sequential, everything depends on this)
# ============================================================================
@ -1389,23 +974,19 @@ run_step_sync "base" install_base_deps || true
wait_for_running_steps
# ============================================================================
# Phase 3: Node + Python + Erlang (parallel, all need mise)
# Phase 3: Node + Erlang (parallel, both need mise)
# ============================================================================
if [ "${STEP_STATUS[mise]}" = "done" ]; then
[ "${STEP_STATUS[node]}" != "skipped" ] && start_step "node" install_node
[ "${STEP_STATUS[python]}" != "skipped" ] && start_step "python" install_python
[ "${STEP_STATUS[erlang]}" != "skipped" ] && start_step "erlang" install_erlang
wait_for_running_steps
fi
# ============================================================================
# Phase 4: Elixir + Claude + OpenCode + Playwright + Tidewave
# (Elixir/Tidewave need Erlang, others need Node)
# Phase 4: Elixir + Claude + Playwright (Elixir needs Erlang, others need Node)
# ============================================================================
[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[elixir]}" != "skipped" ] && start_step "elixir" install_elixir
[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[tidewave]}" != "skipped" ] && start_step "tidewave" install_tidewave
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[claude]}" != "skipped" ] && start_step "claude" install_claude
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[opencode]}" != "skipped" ] && start_step "opencode" install_opencode
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[playwright]}" != "skipped" ] && start_step "playwright" install_playwright
wait_for_running_steps
@ -1480,12 +1061,6 @@ fi
if [ "${STEP_STATUS[claude]}" = "done" ]; then
echo -e "${CYAN}Claude:${NC} claude"
fi
if [ "${STEP_STATUS[opencode]}" = "done" ]; then
echo -e "${CYAN}OpenCode:${NC} opencode"
fi
if [ "${STEP_STATUS[tidewave]}" = "done" ]; then
echo -e "${CYAN}Tidewave:${NC} tidewave (run in Phoenix project dir)"
fi
if [ "${STEP_STATUS[ollama]}" = "done" ]; then
echo -e "${CYAN}Ollama:${NC} ollama run llama3.2"
fi

File diff suppressed because it is too large Load diff