From 3ada749410bacec0cc155716db1861fbf6692f25 Mon Sep 17 00:00:00 2001 From: aaddrick Date: Sat, 21 Mar 2026 18:33:00 -0400 Subject: [PATCH] feat: make bubblewrap the default cowork isolation backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap auto-detection order from KVM → bwrap → host to bwrap → KVM → host. KVM remains available via COWORK_VM_BACKEND=kvm. - detectBackend(): check bwrap before KVM - --doctor: bwrap checked first; KVM deps shown as info (not warnings) unless COWORK_VM_BACKEND=kvm is set - Fix header comment inaccuracy about rootfs.qcow2 check - Update README and handover docs to reflect new default Fixes #326 Co-Authored-By: Claude --- README.md | 6 ++-- docs/cowork-linux-handover.md | 16 ++++----- scripts/cowork-vm-service.js | 30 ++++++++--------- scripts/launcher-common.sh | 63 ++++++++++++++++++++++++----------- 4 files changed, 70 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 79203f3..ac9efbd 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ This project provides build scripts to run Claude Desktop natively on Linux syst > > | Backend | Isolation | Requirements | > |---------|-----------|-------------| -> | **KVM** (preferred) | Full VM via QEMU/KVM | `/dev/kvm`, `qemu-system-x86_64`, `/dev/vhost-vsock`, `socat`, `virtiofsd` | -> | **bubblewrap** (fallback) | Namespace sandbox | `bwrap` installed and functional | +> | **bubblewrap** (default) | Namespace sandbox | `bwrap` installed and functional | +> | **KVM** (opt-in) | Full VM via QEMU/KVM | `/dev/kvm`, `qemu-system-x86_64`, `/dev/vhost-vsock`, `socat`, `virtiofsd` | > | **host** (last resort) | None — runs directly on host | No additional requirements | > -> The best available backend is auto-detected at startup. Run `claude-desktop --doctor` to check which backend will be used and which dependencies are missing. +> The best available backend is auto-detected at startup. Run `claude-desktop --doctor` to check which backend will be used and which dependencies are missing. For full VM-level isolation matching the upstream Windows (Hyper-V) behavior, set `COWORK_VM_BACKEND=kvm`. > > **Note:** The bubblewrap backend mounts your home directory as read-only (only the project working directory is writable). The host backend provides no isolation — use it only if you understand the security implications. diff --git a/docs/cowork-linux-handover.md b/docs/cowork-linux-handover.md index df8635d..c69c7f2 100644 --- a/docs/cowork-linux-handover.md +++ b/docs/cowork-linux-handover.md @@ -16,8 +16,8 @@ cowork-vm-service (Node.js daemon) → delegates to this.backend (auto-detected or COWORK_VM_BACKEND override) Backend selection (priority order): - 1. KvmBackend — QEMU/KVM + vsock + virtiofs (full VM isolation) - 2. BwrapBackend — bubblewrap namespace sandbox (lightweight isolation) + 1. BwrapBackend — bubblewrap namespace sandbox (default) + 2. KvmBackend — QEMU/KVM + vsock + virtiofs (opt-in, full VM isolation) 3. HostBackend — direct on host, no isolation (fallback) KvmBackend path: @@ -173,15 +173,15 @@ The `detectBackend()` function selects the active backend at daemon startup. The ### Auto-detection order: -1. **KVM** — Requires ALL of: +1. **Bwrap** (default) — Requires: + - `bwrap` in PATH + - `bwrap --ro-bind / / true` succeeds (functional test) + +2. **KVM** (opt-in via `COWORK_VM_BACKEND=kvm`) — Requires ALL of: - `/dev/kvm` readable and writable - `qemu-system-x86_64` in PATH - `/dev/vhost-vsock` readable - - `~/.local/share/claude-desktop/vm/rootfs.qcow2` exists - -2. **Bwrap** — Requires: - - `bwrap` in PATH - - `bwrap --ro-bind / / true` succeeds (functional test) + - Rootfs image checked at `startVM()` time, not during detection 3. **Host** — Always available (fallback) diff --git a/scripts/cowork-vm-service.js b/scripts/cowork-vm-service.js index 0579c66..3b52c47 100644 --- a/scripts/cowork-vm-service.js +++ b/scripts/cowork-vm-service.js @@ -12,9 +12,9 @@ * - KvmBackend: QEMU/KVM virtual machine with vsock communication * * Backend selection (auto-detected or overridden via COWORK_VM_BACKEND env): - * 1. kvm — if /dev/kvm, qemu-system-x86_64, /dev/vhost-vsock, and - * rootfs.qcow2 are all available - * 2. bwrap — if bwrap is installed and functional + * 1. bwrap — if bwrap is installed and functional (default) + * 2. kvm — if /dev/kvm, qemu-system-x86_64, and /dev/vhost-vsock + * are available (rootfs checked at startVM time) * 3. host — fallback, no isolation * * Protocol: @@ -1806,7 +1806,18 @@ function detectBackend(emitEvent) { } } - // Auto-detect: try KVM first, then bwrap, then host. + // Auto-detect: try bwrap first, then KVM, then host. + try { + execFileSync('which', ['bwrap'], { stdio: 'pipe' }); + execFileSync('bwrap', ['--ro-bind', '/', '/', 'true'], { + stdio: 'pipe', timeout: 5000 + }); + log('Backend: bwrap'); + return new BwrapBackend(emitEvent); + } catch (e) { + log(`bwrap not available: ${e.message}`); + } + // Note: rootfs is NOT checked here — the app downloads it to // bundlePath which isn't known until startVM(). The rootfs // check happens at startVM time instead. @@ -1820,17 +1831,6 @@ function detectBackend(emitEvent) { log(`KVM not available: ${e.message}`); } - try { - execFileSync('which', ['bwrap'], { stdio: 'pipe' }); - execFileSync('bwrap', ['--ro-bind', '/', '/', 'true'], { - stdio: 'pipe', timeout: 5000 - }); - log('Backend: bwrap'); - return new BwrapBackend(emitEvent); - } catch (e) { - log(`bwrap not available: ${e.message}`); - } - log('Backend: host (no isolation)'); return new HostBackend(emitEvent); } diff --git a/scripts/launcher-common.sh b/scripts/launcher-common.sh index 2a351d0..69b5479 100755 --- a/scripts/launcher-common.sh +++ b/scripts/launcher-common.sh @@ -468,35 +468,59 @@ print(len(servers)) local _distro_id _distro_id=$(_cowork_distro_id) - # KVM access + # Bubblewrap (default backend) + if command -v bwrap &>/dev/null; then + _pass 'bubblewrap: found' + else + _warn 'bubblewrap: not found' + _info \ + "Fix: $(_cowork_pkg_hint "$_distro_id" bubblewrap)" + fi + + # Use _warn for missing KVM deps only when KVM is explicitly + # requested; otherwise use _info since bwrap is the default. + local _kvm_flag + _kvm_flag="${COWORK_VM_BACKEND:-}" + local _kvm_issue + if [[ ${_kvm_flag,,} == kvm ]]; then + _kvm_issue=_warn + else + _kvm_issue=_info + fi + + # KVM backend (opt-in via COWORK_VM_BACKEND=kvm) if [[ -e /dev/kvm ]]; then if [[ -r /dev/kvm && -w /dev/kvm ]]; then _pass 'KVM: accessible' else - _warn 'KVM: /dev/kvm exists but not accessible' + "$_kvm_issue" 'KVM: /dev/kvm exists but not accessible' _info "Fix: sudo usermod -aG kvm $USER" _info '(Log out and back in after running this)' fi else - _warn 'KVM: not available' - _info 'Fix: Install qemu-kvm and ensure KVM is enabled in BIOS' + "$_kvm_issue" 'KVM: not available' + if [[ ${_kvm_flag,,} == kvm ]]; then + _info \ + 'Fix: Install qemu-kvm and ensure KVM is enabled in BIOS' + fi fi # vsock module if [[ -e /dev/vhost-vsock ]]; then _pass 'vsock: module loaded' else - _warn 'vsock: /dev/vhost-vsock not found' - _info 'Fix: sudo modprobe vhost_vsock' + "$_kvm_issue" 'vsock: /dev/vhost-vsock not found' + if [[ ${_kvm_flag,,} == kvm ]]; then + _info 'Fix: sudo modprobe vhost_vsock' + fi fi - # Check required tools: label, binary, pkg-hint name + # KVM tools: QEMU, socat, virtiofsd local _tool_label _tool_bin _tool_pkg for _tool_label in \ 'QEMU:qemu-system-x86_64:qemu' \ 'socat:socat:socat' \ - 'virtiofsd:virtiofsd:virtiofsd' \ - 'bubblewrap:bwrap:bubblewrap' + 'virtiofsd:virtiofsd:virtiofsd' do _tool_bin="${_tool_label#*:}" _tool_pkg="${_tool_bin#*:}" @@ -506,9 +530,11 @@ print(len(servers)) if command -v "$_tool_bin" &>/dev/null; then _pass "$_tool_label: found" else - _warn "$_tool_label: not found" - _info \ - "Fix: $(_cowork_pkg_hint "$_distro_id" "$_tool_pkg")" + "$_kvm_issue" "$_tool_label: not found" + if [[ ${_kvm_flag,,} == kvm ]]; then + _info \ + "Fix: $(_cowork_pkg_hint "$_distro_id" "$_tool_pkg")" + fi fi done @@ -526,15 +552,14 @@ print(len(servers)) # Determine active backend (matches daemon's detectBackend()) local cowork_backend='none (host-direct, no isolation)' - if [[ -e /dev/kvm ]] \ - && [[ -r /dev/kvm && -w /dev/kvm ]] \ - && command -v qemu-system-x86_64 &>/dev/null \ - && [[ -e /dev/vhost-vsock ]] \ - && [[ -f $vm_image ]]; then - cowork_backend='KVM (full VM isolation)' - elif command -v bwrap &>/dev/null \ + if command -v bwrap &>/dev/null \ && bwrap --ro-bind / / true &>/dev/null; then cowork_backend='bubblewrap (namespace sandbox)' + elif [[ -e /dev/kvm ]] \ + && [[ -r /dev/kvm && -w /dev/kvm ]] \ + && command -v qemu-system-x86_64 &>/dev/null \ + && [[ -e /dev/vhost-vsock ]]; then + cowork_backend='KVM (full VM isolation)' fi _info "Cowork isolation: $cowork_backend"