Skip to content

feat: add --workspace flag for host directory mounting#160

Merged
aniketmaurya merged 3 commits intomainfrom
feat/workspace-mount
Apr 13, 2026
Merged

feat: add --workspace flag for host directory mounting#160
aniketmaurya merged 3 commits intomainfrom
feat/workspace-mount

Conversation

@aniketmaurya
Copy link
Copy Markdown
Collaborator

@aniketmaurya aniketmaurya commented Apr 13, 2026

Summary

  • Adds --mount HOST_PATH[:GUEST_PATH] flag to smolvm create that mounts a host directory inside the guest
  • Host directory is read-only (virtio-9p passthrough) with an overlayfs layer on top — the agent can read and write freely, but host files are never modified
  • QEMU-only (macOS first); non-QEMU backends reject workspace mounts with a clear error message
  • SDK users get a mounts= kwarg on SmolVM() and a WorkspaceMount type for advanced use

How it works

Host: ~/my-project ──(virtio-9p, read-only)──→ Guest: /mnt/.smolvm-ws-lower
                                                         +
                                               Guest: /tmp/.smolvm-ws-upper (overlay)
                                                         =
                                               Guest: /workspace (merged, read-write)

Usage

# Mount a project directory (defaults to /workspace inside the sandbox)
smolvm create --mount ~/Projects/my-app

# Mount at a custom guest path
smolvm create --mount ~/Projects/my-app:/code

# Multiple mounts
smolvm create --mount ~/Projects/my-app --mount ~/data:/mnt/data

# SSH in and work
smolvm ssh <sandbox-id>
cat /workspace/README.md     # reads from host
echo "new" > /workspace/x.py # writes to overlay (host untouched)

Python SDK

from smolvm import SmolVM

# Simple — single mount
with SmolVM(mounts=["~/Projects/my-app"]) as vm:
    result = vm.run("ls /workspace")
    print(result.stdout)

# Custom guest path
with SmolVM(mounts=["~/Projects/my-app:/code"]) as vm:
    result = vm.run("cat /code/README.md")

# Multiple mounts
with SmolVM(mounts=["~/src:/code", "~/data:/mnt/data"]) as vm:
    result = vm.run("ls /code /mnt/data")

# Advanced — using WorkspaceMount directly
from smolvm import SmolVM, VMConfig, WorkspaceMount

config = VMConfig(
    ...,
    workspace_mounts=[
        WorkspaceMount(host_path="~/Projects/my-app", guest_path="/code"),
        WorkspaceMount(host_path="~/data", guest_path="/mnt/data"),
    ],
)
with SmolVM(config) as vm:
    result = vm.run("ls /code")

Closes #157

Test plan

  • 25 tests in tests/test_workspace.py covering:
    • WorkspaceMount validation (valid dir, nonexistent path, file-not-dir, relative guest_path, custom tag/path, resolved_tag)
    • VMConfig.workspace_mounts (default empty, duplicate guest_path rejected, duplicate tag rejected)
    • QEMU command builder (asserts -fsdev and virtio-9p-device args with readonly=on, security_model=mapped-xattr)
    • Non-QEMU backend rejection (Firecracker + libkrun)
    • Snapshot guard (blocks VMs with workspace mounts)
    • CLI flag parsing (--mount single, with guest path, multiples, default None)
    • _parse_mount_specs (host-only, host:guest, indexed defaults, mixed)
    • Facade guard (non-root SSH user rejected)
  • Full test suite passes (575 passed, 9 pre-existing async skips)
  • Linter clean (no new issues)
  • Manual: smolvm create --mount /tmp/test-dir && smolvm ssh <id> → verify /workspace is mounted and writable

🤖 Generated with Claude Code

…+ overlayfs

Mount a host directory read-only into the guest via QEMU's virtio-9p
passthrough, with an overlayfs layer on top so the agent can read and
write freely inside /workspace without touching the host files.

- New WorkspaceMount type and VMConfig.workspace_mounts field
- QEMU command builder emits -fsdev + virtio-9p-device/pci args
- Guest-side mount via SSH: modprobe 9p, mount 9p lower, overlay on top
- Firecracker backend rejects workspace mounts with clear error
- Snapshot guard blocks VMs with workspace mounts
- CLI: smolvm create --workspace ~/my-project
- SDK: SmolVM(config, workspace="~/my-project")

Closes #157

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a workspace mounting feature to SmolVM. Users can now mount host directories into guest VMs via the --workspace CLI flag. New WorkspaceMount type and workspace_mounts configuration field enable 9p-based sharing in QEMU, with validation and mounting logic during VM startup. Firecracker backend rejects workspace mounts.

Changes

Cohort / File(s) Summary
Type Definitions
src/smolvm/types.py
Added WorkspaceMount model with host_path, guest_path, and mount_tag fields, including validators for directory existence and absolute path requirements. Extended VMConfig with workspace_mounts field and validator ensuring unique mount tags and guest paths.
CLI Integration
src/smolvm/cli.py
Added --workspace PATH flag to smolvm create command, parsed as pathlib.Path and passed to VM construction.
VM Facade & Lifecycle
src/smolvm/facade.py
Extended SmolVM.__init__ with workspace parameter that normalizes to WorkspaceMount entries. Added validation preventing workspace use with VM reconnection or existing mounts. Implemented _mount_workspaces() helper in start() and async_start() to execute remote mount operations via modprobe and 9p overlay mounting, with specialized error handling for missing kernel support.
VM Backend & QEMU
src/smolvm/vm.py
Added workspace mount validation to reject Firecracker backend. Extended QEMU command construction in _start_qemu() and _async_start_qemu() to generate -fsdev and virtio-9p-device arguments for each configured mount. Added snapshot validation to block snapshots when workspace mounts are present.
Package Exports
src/smolvm/__init__.py
Added WorkspaceMount to package-level exports via __all__.
Test Updates
tests/test_cli.py, tests/test_facade.py
Updated mock assertions to include workspace=None parameter in CLI tests. Added workspace_mounts=[] to mock VM configs in facade lifecycle tests.
Comprehensive Test Suite
tests/test_workspace.py
New test module covering Pydantic validation, VMConfig field constraints, QEMU command generation with 9p devices, Firecracker rejection, snapshot blocking, and CLI parsing behavior.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI
    participant Facade as SmolVM Facade
    participant Config as VMConfig
    participant QEMU
    participant Guest
    
    User->>CLI: smolvm create --workspace /path
    CLI->>Facade: SmolVM(config, workspace=/path)
    Facade->>Facade: Validate & normalize workspace
    Facade->>Config: Add to workspace_mounts
    Note over Config: workspace_mounts = [WorkspaceMount(...)]
    
    User->>Facade: vm.start()
    Facade->>Facade: Check can_run_commands()
    Facade->>Facade: Wait for SSH ready
    Facade->>Guest: modprobe 9pnet_virtio
    Facade->>Guest: mount -t 9p with overlayfs
    Guest->>Guest: Mount succeeded or error
    
    Facade->>QEMU: (during boot) -fsdev local,id=fsdev-workspace0
    QEMU->>QEMU: -device virtio-9p-pci,fsdev=fsdev-workspace0
    Note over Guest: 9p share available at guest_path
    Guest->>User: ✓ Workspace mounted and accessible
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #118: Adds image parameter to SmolVM.__init__ and updates package-level exports in __init__.py, mirroring the API surface extension pattern used for the workspace parameter.
  • PR #39: Modifies VMConfig with mount-related fields and extends QEMU startup command construction for guest-side resources, similar architectural approach to workspace mount handling.

Suggested labels

QEMU

Poem

🏔️ Shared folders now unite the host and guest,
Nine-P mounts them true—a strategic conquest.
By workspace flag, we stake our sacred ground,
Where files flow both ways, all safe and sound.
Control the boundaries, yet blur the lines—
That's how a proper architecture shines.


Right then. Listen carefully. What you're looking at here is a carefully orchestrated feature—nothing careless, nothing hasty. The workspace mounting system works like a proper operation: every component in its place, every validation deliberate.

The facade layer, that's your command structure. It takes the workspace path, vets it proper, ensures it's legitimate before anything gets mounted. In start(), there's a sequence, yeah? SSH ready first, then the mount commands. You don't rush into unmapped territory.

The QEMU configuration—that's your territory now. The -fsdev and virtio-9p-device arguments, they're your perimeter. Firecracker? Can't operate there. It simply doesn't have the infrastructure. You work with what you've got, and you work it right.

The tests, they're comprehensive. Every angle covered—validation, rejection paths, the whole picture. Someone's thought this through, and that matters.

Now, this is a solid bit of work. Just remember: it's not just about the code. It's about understanding why each guard rail's there, why certain backends are rejected, why the sequence matters. Read it like you'd read a map before a job. The details will tell you everything you need to know.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature addition: a --workspace flag enabling host directory mounting into guest VMs.
Linked Issues check ✅ Passed The implementation fully satisfies issue #157: provides shared folder mounting between host and guest with read-only host-side passthrough and read-write overlay access, supporting development workflows as requested.
Out of Scope Changes check ✅ Passed All changes are focused on the workspace mounting feature: CLI flag, SDK argument, validation models, QEMU command building, and comprehensive tests with no unrelated modifications.
Description check ✅ Passed The pull request description is well-structured, complete, and addresses all template requirements with clear examples and test coverage details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/workspace-mount

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added the QEMU label Apr 13, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/smolvm/facade.py`:
- Around line 1543-1595: In _mount_workspaces, guard the privileged
modprobe/mount operations by verifying the SSH session is root before running
mount_script: run a quick command like 'id -u' via self._ssh (or check an
existing self._ssh.user/is_root property if available) and if the returned UID
is not 0 raise a SmolVMError (include vm_id and mount_tag) that clearly
instructs the caller to use a root SSH account or open a dedicated root session
for workspace mounting; alternatively implement opening a dedicated root SSH
connection for the mount steps (use a helper like start()/a new root SSH method)
if you prefer automatic escalation instead of failing fast.

In `@src/smolvm/types.py`:
- Around line 362-382: The validator validate_workspace_mounts currently
computes a fallback tag (mount_tag or f"workspace{index}") but doesn't persist
it (models are frozen), forcing vm.py to duplicate the same logic; fix by
centralizing tag generation and persisting it: either (A) create a single helper
generate_workspace_tag(index) and use it from validate_workspace_mounts and the
callers in vm.py, or (B) have validate_workspace_mounts assign the computed tag
back into each WorkspaceMount by replacing the item with a new instance (e.g.,
using WorkspaceMount.model_copy(update={"mount_tag": tag}) or equivalent) when
mount.mount_tag is falsy so downstream code (vm.py) can rely on mount.mount_tag
being set; update references to validate_workspace_mounts and vm.py usage
accordingly.

In `@src/smolvm/vm.py`:
- Around line 750-755: The current workspace_mounts guard only blocks
BACKEND_FIRECRACKER; change the logic to allow workspace_mounts only for
BACKEND_QEMU (e.g., check backend != BACKEND_QEMU and raise SmolVMError) in the
existing block (the function where effective_config.workspace_mounts is checked)
so Libkrun and any other non-QEMU backends are rejected up front; additionally
add the same backend != BACKEND_QEMU guard to SmolVMManager.async_create() (and
ensure you do not rely on _start_libkrun() to handle 9p wiring) so
async_create() also throws the contract error when
effective_config.workspace_mounts is present for non-QEMU backends.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f28d8f22-a3b8-418f-9bf5-10401f2731ec

📥 Commits

Reviewing files that changed from the base of the PR and between 7b9c9f9 and 5538d5b.

📒 Files selected for processing (8)
  • src/smolvm/__init__.py
  • src/smolvm/cli.py
  • src/smolvm/facade.py
  • src/smolvm/types.py
  • src/smolvm/vm.py
  • tests/test_cli.py
  • tests/test_facade.py
  • tests/test_workspace.py

aniketmaurya and others added 2 commits April 14, 2026 00:35
…ogic

- Add fail-fast check in _mount_workspaces for non-root ssh_user
- Block workspace mounts on all non-QEMU backends (not just Firecracker)
- Add resolved_tag() helper to WorkspaceMount, use it everywhere
- Add tests: libkrun rejection, resolved_tag, non-root SSH guard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yntax

- `--mount ~/project` mounts at /workspace (single mount default)
- `--mount ~/project:/code` mounts at custom guest path
- `--mount ~/a --mount ~/b:/data` supports multiple mounts
- Colon-separated syntax avoids argparse ambiguity with space-separated args
- SDK kwarg renamed from workspace= to mounts= (list of spec strings)
- _parse_mount_specs() handles the HOST[:GUEST] parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aniketmaurya aniketmaurya merged commit edf214c into main Apr 13, 2026
10 checks passed
@aniketmaurya aniketmaurya deleted the feat/workspace-mount branch April 13, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Shared folders between guest and host

1 participant