No description
Find a file
Thales Maciel fc72fcd3a1 Add guest-only workspace secrets
Add explicit workspace secrets across the CLI, SDK, and MCP, with create-time secret definitions and per-call secret-to-env mapping for exec, shell open, and service start. Persist only safe secret metadata in workspace records, materialize secret files under /run/pyro-secrets, and redact secret values from exec output, shell reads, service logs, and surfaced errors.

Fix the remaining real-guest shell gap by shipping bundled guest init alongside the guest agent and patching both into guest-backed workspace rootfs images before boot. The new init mounts devpts so PTY shells work on Firecracker guests, while reset continues to recreate the sandbox and re-materialize secrets from stored task-local secret material.

Validation: uv lock; UV_CACHE_DIR=.uv-cache make check; UV_CACHE_DIR=.uv-cache make dist-check; and a real guest-backed Firecracker smoke covering workspace create with secrets, secret-backed exec, shell, service, reset, and delete.
2026-03-12 15:43:34 -03:00
docs Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
examples Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
runtime_sources Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
scripts Add terminal walkthrough assets 2026-03-09 22:49:56 -03:00
src/pyro_mcp Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
tests Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
.gitattributes Bootstrap pyro_mcp v0.0.1 with MCP static tool and Ollama demo 2026-03-05 15:41:57 -03:00
.gitignore Add adoption-focused examples, contract docs, and CLI polish 2026-03-07 22:34:14 -03:00
.pre-commit-config.yaml Bootstrap pyro_mcp v0.0.1 with MCP static tool and Ollama demo 2026-03-05 15:41:57 -03:00
.python-version Bootstrap pyro_mcp v0.0.1 with MCP static tool and Ollama demo 2026-03-05 15:41:57 -03:00
AGENTS.md Remove GitHub-specific project plumbing 2026-03-09 22:58:29 -03:00
CHANGELOG.md Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
LICENSE Refactor public API around environments 2026-03-08 16:02:02 -03:00
Makefile Add Make target for PyPI publishing 2026-03-09 19:26:57 -03:00
pyproject.toml Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
README.md Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00
TASKS.tmp.md Add adoption-focused examples, contract docs, and CLI polish 2026-03-07 22:34:14 -03:00
uv.lock Add guest-only workspace secrets 2026-03-12 15:43:34 -03:00

pyro-mcp

pyro-mcp runs one-shot commands and repeated workspaces inside ephemeral Firecracker microVMs using curated Linux environments such as debian:12.

PyPI version

This is for coding agents, MCP clients, and developers who want isolated command execution in ephemeral microVMs.

It exposes the same runtime in three public forms:

  • the pyro CLI
  • the Python SDK via from pyro_mcp import Pyro
  • an MCP server so LLM clients can call VM tools directly

Start Here

Quickstart

Use either of these equivalent quickstart paths:

# Package without install
python -m pip install uv
uvx --from pyro-mcp pyro doctor
uvx --from pyro-mcp pyro env list
uvx --from pyro-mcp pyro env pull debian:12
uvx --from pyro-mcp pyro run debian:12 -- git --version

Quickstart walkthrough

# Already installed
pyro doctor
pyro env list
pyro env pull debian:12
pyro run debian:12 -- git --version

From a repo checkout, replace pyro with uv run pyro.

What success looks like:

Platform: linux-x86_64
Runtime: PASS
Catalog version: 2.9.0
...
[pull] phase=install environment=debian:12
[pull] phase=ready environment=debian:12
Pulled: debian:12
...
[run] phase=create environment=debian:12
[run] phase=start vm_id=...
[run] phase=execute vm_id=...
[run] environment=debian:12 execution_mode=guest_vsock exit_code=0 duration_ms=...
git version ...

The first pull downloads an OCI environment from public Docker Hub, requires outbound HTTPS access to registry-1.docker.io, and needs local cache space for the guest image.

After the quickstart works:

  • prove the full one-shot lifecycle with uvx --from pyro-mcp pyro demo
  • create a persistent workspace with uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo
  • update a live workspace from the host with uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes
  • add literal or file-backed secrets with uvx --from pyro-mcp pyro workspace create debian:12 --secret API_TOKEN=expected --secret-file PIP_TOKEN=./token.txt
  • map one persisted secret into one exec, shell, or service call with --secret-env API_TOKEN
  • diff the live workspace against its create-time baseline with uvx --from pyro-mcp pyro workspace diff WORKSPACE_ID
  • capture a checkpoint with uvx --from pyro-mcp pyro workspace snapshot create WORKSPACE_ID checkpoint
  • reset a broken workspace with uvx --from pyro-mcp pyro workspace reset WORKSPACE_ID --snapshot checkpoint
  • export a changed file or directory with uvx --from pyro-mcp pyro workspace export WORKSPACE_ID note.txt --output ./note.txt
  • open a persistent interactive shell with uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID
  • start long-running workspace services with uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID app --ready-file .ready -- sh -lc 'touch .ready && while true; do sleep 60; done'
  • move to Python or MCP via docs/integrations.md

Supported Hosts

Supported today:

  • Linux x86_64
  • Python 3.12+
  • uv
  • /dev/kvm

Optional for outbound guest networking:

  • ip
  • nft or iptables
  • privilege to create TAP devices and configure NAT

Not supported today:

  • macOS
  • Windows
  • Linux hosts without working KVM at /dev/kvm

Detailed Walkthrough

If you want the expanded version of the canonical quickstart, use the step-by-step flow below.

1. Check the host

uvx --from pyro-mcp pyro doctor

Expected success signals:

Platform: linux-x86_64
Runtime: PASS
KVM: exists=yes readable=yes writable=yes
Environment cache: /home/you/.cache/pyro-mcp/environments
Capabilities: vm_boot=yes guest_exec=yes guest_network=yes
Networking: tun=yes ip_forward=yes

2. Inspect the catalog

uvx --from pyro-mcp pyro env list

Expected output:

Catalog version: 2.9.0
debian:12 [installed|not installed] Debian 12 environment with Git preinstalled for common agent workflows.
debian:12-base [installed|not installed] Minimal Debian 12 environment for shell and core Unix tooling.
debian:12-build [installed|not installed] Debian 12 environment with Git and common build tools preinstalled.

3. Pull the default environment

uvx --from pyro-mcp pyro env pull debian:12

The first pull downloads an OCI environment from public Docker Hub, requires outbound HTTPS access to registry-1.docker.io, and needs local cache space for the guest image. See docs/host-requirements.md for the full host requirements.

4. Run one command in a guest

uvx --from pyro-mcp pyro run debian:12 -- git --version

Expected success signals:

[run] environment=debian:12 execution_mode=guest_vsock exit_code=0 duration_ms=...
git version ...

The guest command output and the [run] ... summary are written to different streams, so they may appear in either order in terminals or capture tools. Use --json if you need a deterministic structured result.

5. Optional demos

uvx --from pyro-mcp pyro demo
uvx --from pyro-mcp pyro demo --network

pyro demo proves the one-shot create/start/exec/delete VM lifecycle works end to end.

Example output:

{
  "cleanup": {
    "deleted": true,
    "reason": "post_exec_cleanup",
    "vm_id": "..."
  },
  "command": "git --version",
  "environment": "debian:12",
  "execution_mode": "guest_vsock",
  "exit_code": 0,
  "stdout": "git version ...\n"
}

When you are done evaluating and want to remove stale cached environments, run pyro env prune.

If you prefer a fuller copy-pasteable transcript, see docs/first-run.md. The walkthrough GIF above was rendered from docs/assets/first-run.tape using scripts/render_tape.sh.

Persistent Workspaces

Use pyro run for one-shot commands. Use pyro workspace ... when you need repeated commands in one workspace without recreating the sandbox every time.

The project direction is an agent workspace, not a CI job runner. Persistent workspaces are meant to let an agent stay inside one bounded sandbox across multiple steps. See docs/vision.md for the product thesis and the longer-term interaction model.

pyro workspace create debian:12 --seed-path ./repo
pyro workspace create debian:12 --seed-path ./repo --secret API_TOKEN=expected
pyro workspace sync push WORKSPACE_ID ./changes --dest src
pyro workspace exec WORKSPACE_ID -- cat src/note.txt
pyro workspace exec WORKSPACE_ID --secret-env API_TOKEN -- sh -lc 'test "$API_TOKEN" = "expected"'
pyro workspace diff WORKSPACE_ID
pyro workspace snapshot create WORKSPACE_ID checkpoint
pyro workspace reset WORKSPACE_ID --snapshot checkpoint
pyro workspace reset WORKSPACE_ID
pyro workspace export WORKSPACE_ID src/note.txt --output ./note.txt
pyro workspace shell open WORKSPACE_ID --secret-env API_TOKEN
pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd'
pyro workspace shell read WORKSPACE_ID SHELL_ID
pyro workspace shell close WORKSPACE_ID SHELL_ID
pyro workspace service start WORKSPACE_ID web --secret-env API_TOKEN --ready-file .web-ready -- sh -lc 'touch .web-ready && while true; do sleep 60; done'
pyro workspace service start WORKSPACE_ID worker --ready-file .worker-ready -- sh -lc 'touch .worker-ready && while true; do sleep 60; done'
pyro workspace service list WORKSPACE_ID
pyro workspace service status WORKSPACE_ID web
pyro workspace service logs WORKSPACE_ID web --tail-lines 50
pyro workspace service stop WORKSPACE_ID web
pyro workspace service stop WORKSPACE_ID worker
pyro workspace logs WORKSPACE_ID
pyro workspace delete WORKSPACE_ID

Persistent workspaces start in /workspace and keep command history until you delete them. For machine consumption, add --json and read the returned workspace_id. Use --seed-path when you want the workspace to start from a host directory or a local .tar / .tar.gz / .tgz archive instead of an empty workspace. Use pyro workspace sync push when you want to import later host-side changes into a started workspace. Sync is non-atomic in 2.9.0; if it fails partway through, prefer pyro workspace reset to recover from baseline or one named snapshot. Use pyro workspace diff to compare the live /workspace tree to its immutable create-time baseline, and pyro workspace export to copy one changed file or directory back to the host. Use pyro workspace snapshot * and pyro workspace reset when you want explicit checkpoints and full-sandbox recovery. Use pyro workspace exec for one-shot non-interactive commands inside a live workspace, and pyro workspace shell * when you need a persistent PTY session that keeps interactive shell state between calls. Use pyro workspace service * when the workspace needs one or more long-running background processes. Typed readiness checks prefer --ready-file, --ready-tcp, or --ready-http; keep --ready-command as the escape hatch. Service metadata and logs live outside /workspace, so the internal service state does not appear in pyro workspace diff or pyro workspace export. Use --secret and --secret-file at workspace creation when the sandbox needs private tokens or config. Persisted secrets are materialized inside the guest at /run/pyro-secrets/<name>, and --secret-env SECRET_NAME[=ENV_VAR] maps one secret into one exec, shell, or service call without exposing the raw value in workspace status, logs, diffs, or exports.

Public Interfaces

The public user-facing interface is pyro and Pyro. After the CLI validation path works, you can choose one of three surfaces:

  • pyro for direct CLI usage, including one-shot run and persistent workspace workflows
  • from pyro_mcp import Pyro for Python orchestration
  • pyro mcp serve for MCP clients

Command forms:

  • published package without install: uvx --from pyro-mcp pyro ...
  • installed package: pyro ...
  • source checkout: uv run pyro ...

Makefile targets are contributor conveniences for this repository and are not the primary product UX.

Official Environments

Current official environments in the shipped catalog:

  • debian:12
  • debian:12-base
  • debian:12-build

The package ships the embedded Firecracker runtime and a package-controlled environment catalog. Official environments are pulled as OCI artifacts from public Docker Hub repositories into a local cache on first use or through pyro env pull. End users do not need registry credentials to pull or run official environments. The default cache location is ~/.cache/pyro-mcp/environments; override it with PYRO_ENVIRONMENT_CACHE_DIR.

CLI

List available environments:

pyro env list

Prefetch one environment:

pyro env pull debian:12

Run one command in an ephemeral VM:

pyro run debian:12 -- git --version

Run with outbound internet enabled:

pyro run debian:12 --network -- \
  'python3 -c "import urllib.request; print(urllib.request.urlopen(\"https://example.com\", timeout=10).status)"'

Show runtime and host diagnostics:

pyro doctor
pyro doctor --json

pyro run defaults to 1 vCPU / 1024 MiB. It fails closed when guest boot or guest exec is unavailable. Use --allow-host-compat only if you explicitly want host execution.

Run the MCP server after the CLI path above works:

pyro mcp serve

Run the deterministic demo:

pyro demo
pyro demo --network

Run the Ollama demo:

ollama serve
ollama pull llama3.2:3b
pyro demo ollama

Python SDK

from pyro_mcp import Pyro

pyro = Pyro()
result = pyro.run_in_vm(
    environment="debian:12",
    command="git --version",
    timeout_seconds=30,
    network=False,
)
print(result["stdout"])

Lower-level lifecycle control remains available:

from pyro_mcp import Pyro

pyro = Pyro()
created = pyro.create_vm(
    environment="debian:12",
    ttl_seconds=600,
    network=True,
)
vm_id = created["vm_id"]
pyro.start_vm(vm_id)
result = pyro.exec_vm(vm_id, command="git --version", timeout_seconds=30)
print(result["stdout"])

exec_vm() is a one-command auto-cleaning call. After it returns, the VM is already deleted.

Environment management is also available through the SDK:

from pyro_mcp import Pyro

pyro = Pyro()
print(pyro.list_environments())
print(pyro.inspect_environment("debian:12"))

For repeated commands in one workspace:

from pyro_mcp import Pyro

pyro = Pyro()
workspace = pyro.create_workspace(environment="debian:12", seed_path="./repo")
workspace_id = workspace["workspace_id"]
try:
    pyro.push_workspace_sync(workspace_id, "./changes", dest="src")
    result = pyro.exec_workspace(workspace_id, command="cat src/note.txt")
    print(result["stdout"], end="")
finally:
    pyro.delete_workspace(workspace_id)

MCP Tools

Primary agent-facing tool:

  • vm_run(environment, command, vcpu_count=1, mem_mib=1024, timeout_seconds=30, ttl_seconds=600, network=false, allow_host_compat=false)

Advanced lifecycle tools:

  • vm_list_environments()
  • vm_create(environment, vcpu_count=1, mem_mib=1024, ttl_seconds=600, network=false, allow_host_compat=false)
  • vm_start(vm_id)
  • vm_exec(vm_id, command, timeout_seconds=30) auto-cleans the VM after that command
  • vm_stop(vm_id)
  • vm_delete(vm_id)
  • vm_status(vm_id)
  • vm_network_info(vm_id)
  • vm_reap_expired()

Persistent workspace tools:

  • workspace_create(environment, vcpu_count=1, mem_mib=1024, ttl_seconds=600, network=false, allow_host_compat=false, seed_path=null, secrets=null)
  • workspace_sync_push(workspace_id, source_path, dest="/workspace")
  • workspace_exec(workspace_id, command, timeout_seconds=30, secret_env=null)
  • workspace_export(workspace_id, path, output_path)
  • workspace_diff(workspace_id)
  • snapshot_create(workspace_id, snapshot_name)
  • snapshot_list(workspace_id)
  • snapshot_delete(workspace_id, snapshot_name)
  • workspace_reset(workspace_id, snapshot="baseline")
  • service_start(workspace_id, service_name, command, cwd="/workspace", readiness=null, ready_timeout_seconds=30, ready_interval_ms=500, secret_env=null)
  • service_list(workspace_id)
  • service_status(workspace_id, service_name)
  • service_logs(workspace_id, service_name, tail_lines=200)
  • service_stop(workspace_id, service_name)
  • shell_open(workspace_id, cwd="/workspace", cols=120, rows=30, secret_env=null)
  • shell_read(workspace_id, shell_id, cursor=0, max_chars=65536)
  • shell_write(workspace_id, shell_id, input, append_newline=true)
  • shell_signal(workspace_id, shell_id, signal_name="INT")
  • shell_close(workspace_id, shell_id)
  • workspace_status(workspace_id)
  • workspace_logs(workspace_id)
  • workspace_delete(workspace_id)

Integration Examples

Runtime

The package ships an embedded Linux x86_64 runtime payload with:

  • Firecracker
  • Jailer
  • guest agent
  • runtime manifest and diagnostics

No system Firecracker installation is required. pyro installs curated environments into a local cache and reports their status through pyro env inspect and pyro doctor. The public CLI is human-readable by default; add --json for structured output.

Contributor Workflow

For work inside this repository:

make help
make setup
make check
make dist-check

Contributor runtime sources live under runtime_sources/. The packaged runtime bundle under src/pyro_mcp/runtime_bundle/ contains the embedded boot/runtime assets plus manifest metadata; end-user environment installs pull OCI-published environments by default. Use PYRO_RUNTIME_BUNDLE_DIR=build/runtime_bundle only when you are explicitly validating a locally built contributor runtime bundle.

Official environment publication is performed locally against Docker Hub:

export DOCKERHUB_USERNAME='your-dockerhub-username'
export DOCKERHUB_TOKEN='your-dockerhub-token'
make runtime-materialize
make runtime-publish-official-environments-oci

make runtime-publish-environment-oci auto-exports the OCI layout for the selected environment if it is missing. The publisher accepts either DOCKERHUB_USERNAME and DOCKERHUB_TOKEN or OCI_REGISTRY_USERNAME and OCI_REGISTRY_PASSWORD. Docker Hub uploads are chunked by default for large rootfs layers; if you need to tune a slow link, use PYRO_OCI_UPLOAD_TIMEOUT_SECONDS, PYRO_OCI_UPLOAD_CHUNK_SIZE_BYTES, and PYRO_OCI_REQUEST_TIMEOUT_SECONDS.

For a local PyPI publish:

export TWINE_PASSWORD='pypi-...'
make pypi-publish

make pypi-publish defaults TWINE_USERNAME to __token__. Set PYPI_REPOSITORY_URL=https://test.pypi.org/legacy/ to publish to TestPyPI instead.