diff --git a/README.md b/README.md index 793a7b3..65e2dfc 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,50 @@ # pyro-mcp -`pyro-mcp` is a stable agent workspace product for one-shot commands and persistent work inside ephemeral Firecracker microVMs using curated Linux environments such as `debian:12`. +`pyro-mcp` is a disposable MCP workspace for chat-based coding agents such as +Claude Code, Codex, and OpenCode. + +It is built for Linux `x86_64` hosts with working KVM. The product path is: + +1. prove the host works +2. connect a chat host over MCP +3. let the agent work inside a disposable workspace +4. validate the workflow with the recipe-backed smoke pack + +`pyro-mcp` currently has no users. Expect breaking changes while this chat-host +path is still being shaped. + +This repo is not trying to be a generic VM toolkit, a CI runner, or an +SDK-first platform. [![PyPI version](https://img.shields.io/pypi/v/pyro-mcp.svg)](https://pypi.org/project/pyro-mcp/) -This is for coding agents, MCP clients, and developers who want isolated command execution and stable disposable workspaces 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 -- Install: [docs/install.md](docs/install.md) -- Vision: [docs/vision.md](docs/vision.md) -- Workspace GA roadmap: [docs/roadmap/task-workspace-ga.md](docs/roadmap/task-workspace-ga.md) -- LLM chat roadmap: [docs/roadmap/llm-chat-ergonomics.md](docs/roadmap/llm-chat-ergonomics.md) -- Use-case recipes: [docs/use-cases/README.md](docs/use-cases/README.md) +- Install and zero-to-hero path: [docs/install.md](docs/install.md) - First run transcript: [docs/first-run.md](docs/first-run.md) +- Chat host integrations: [docs/integrations.md](docs/integrations.md) +- Use-case recipes: [docs/use-cases/README.md](docs/use-cases/README.md) +- Vision: [docs/vision.md](docs/vision.md) +- Public contract: [docs/public-contract.md](docs/public-contract.md) +- Host requirements: [docs/host-requirements.md](docs/host-requirements.md) +- Troubleshooting: [docs/troubleshooting.md](docs/troubleshooting.md) - Stable workspace walkthrough GIF: [docs/assets/workspace-first-run.gif](docs/assets/workspace-first-run.gif) - Terminal walkthrough GIF: [docs/assets/first-run.gif](docs/assets/first-run.gif) -- PyPI package: [pypi.org/project/pyro-mcp](https://pypi.org/project/pyro-mcp/) - What's new in 4.0.0: [CHANGELOG.md#400](CHANGELOG.md#400) -- Host requirements: [docs/host-requirements.md](docs/host-requirements.md) -- Integration targets: [docs/integrations.md](docs/integrations.md) -- Public contract: [docs/public-contract.md](docs/public-contract.md) -- Troubleshooting: [docs/troubleshooting.md](docs/troubleshooting.md) -- Changelog: [CHANGELOG.md](CHANGELOG.md) +- PyPI package: [pypi.org/project/pyro-mcp](https://pypi.org/project/pyro-mcp/) + +## Who It's For + +- Claude Code users who want disposable workspaces instead of running directly + on the host +- Codex users who want an MCP-backed sandbox for repo setup, bug fixing, and + evaluation loops +- OpenCode users who want the same disposable workspace model +- people evaluating repo setup, test, and app-start workflows from a chat + interface on a clean machine + +If you want a general VM platform, a queueing system, or a broad SDK product, +this repo is intentionally biased away from that story. ## Quickstart @@ -73,84 +89,15 @@ Pulled: debian:12 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. - -## Stable Workspace Path - -`pyro run` is the stable one-shot entrypoint. `pyro workspace ...` is the stable path when an -agent needs one sandbox to stay alive across repeated commands, shells, services, checkpoints, -diffs, exports, and reset. - -After that stable walkthrough works, continue with the recipe set in -[docs/use-cases/README.md](docs/use-cases/README.md). It packages the five core workspace stories -into documented flows plus real guest-backed smoke targets such as `make smoke-use-cases` and -`make smoke-repro-fix-loop`. At this point `make smoke-use-cases` is the -trustworthy guest-backed release-gate path for the advertised workspace workflows. - -The commands below use plain `pyro ...`. Run the same flow with `uvx --from pyro-mcp pyro ...` -for the published package, or `uv run pyro ...` from a source checkout. - -```bash -uv tool install pyro-mcp -WORKSPACE_ID="$(pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 --id-only)" -pyro workspace list -pyro workspace update "$WORKSPACE_ID" --label owner=codex -pyro workspace sync push "$WORKSPACE_ID" ./changes -pyro workspace file read "$WORKSPACE_ID" note.txt --content-only -pyro workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch -pyro workspace exec "$WORKSPACE_ID" -- cat note.txt -pyro workspace snapshot create "$WORKSPACE_ID" checkpoint -pyro workspace service start "$WORKSPACE_ID" web --ready-file .web-ready -- sh -lc 'touch .web-ready && while true; do sleep 60; done' -pyro workspace reset "$WORKSPACE_ID" --snapshot checkpoint -pyro workspace export "$WORKSPACE_ID" note.txt --output ./note.txt -pyro workspace delete "$WORKSPACE_ID" -``` - -![Stable workspace walkthrough](docs/assets/workspace-first-run.gif) - -That stable workspace path gives you: - -- initial host-in seeding with `--seed-path` -- discovery metadata with `--name`, `--label`, `workspace list`, and `workspace update` -- later host-in updates with `workspace sync push` -- model-native file inspection and text edits with `workspace file *` and `workspace patch apply` -- one-shot commands with `workspace exec` and persistent PTYs with `workspace shell *` -- long-running processes with `workspace service *` -- explicit checkpoints with `workspace snapshot *` -- full-sandbox recovery with `workspace reset` -- baseline comparison with `workspace diff` -- explicit host-out export with `workspace export` -- secondary stopped-workspace disk inspection with `workspace stop|start` and `workspace disk *` - -After the quickstart works: - -- prove the full one-shot lifecycle with `uvx --from pyro-mcp pyro demo` -- start most chat hosts with `uvx --from pyro-mcp pyro mcp serve` -- create a persistent workspace with `uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo` -- add a human-friendly workspace name with `uvx --from pyro-mcp pyro workspace create debian:12 --name repro-fix --label issue=123` -- rediscover or retag workspaces with `uvx --from pyro-mcp pyro workspace list` and `uvx --from pyro-mcp pyro workspace update WORKSPACE_ID --label owner=codex` -- update a live workspace from the host with `uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes` -- enable outbound guest networking for one workspace with `uvx --from pyro-mcp pyro workspace create debian:12 --network-policy egress` -- 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` -- inspect and edit files without shell quoting with `uvx --from pyro-mcp pyro workspace file read WORKSPACE_ID src/app.py --content-only`, `uvx --from pyro-mcp pyro workspace file write WORKSPACE_ID src/app.py --text-file ./app.py`, and `uvx --from pyro-mcp pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch` -- 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 --id-only` -- 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'` -- publish one guest service port to the host with `uvx --from pyro-mcp pyro workspace create debian:12 --network-policy egress+published-ports` and `uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app` -- stop a workspace for offline inspection with `uvx --from pyro-mcp pyro workspace stop WORKSPACE_ID` -- inspect or export one stopped guest rootfs with `uvx --from pyro-mcp pyro workspace disk list WORKSPACE_ID`, `uvx --from pyro-mcp pyro workspace disk read WORKSPACE_ID note.txt --content-only`, and `uvx --from pyro-mcp pyro workspace disk export WORKSPACE_ID --output ./workspace.ext4` -- move to Python or MCP via [docs/integrations.md](docs/integrations.md) +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. ## Chat Host Quickstart -For most MCP chat hosts, bare `pyro mcp serve` now starts `workspace-core`. It exposes the practical -persistent editing loop without shells, services, snapshots, secrets, network -policy, or disk tools. +After the quickstart works, the intended next step is to connect a chat host. +Bare `pyro mcp serve` starts `workspace-core`, which is the default product +path. ```bash uvx --from pyro-mcp pyro mcp serve @@ -189,223 +136,55 @@ OpenCode `opencode.json` snippet: } ``` -If `pyro-mcp` is already installed, replace the `uvx --from pyro-mcp pyro` -command with `pyro` in the same host-specific command or config shape. Use -`--profile workspace-full` only when the host truly needs the full advanced -workspace surface. +If `pyro-mcp` is already installed, replace `uvx --from pyro-mcp pyro` with +`pyro` in the same command or config shape. -Profile progression: +Use `--profile workspace-full` only when the chat truly needs shells, services, +snapshots, secrets, network policy, or disk tools. -- `workspace-core`: default and recommended first profile for normal persistent chat editing -- `vm-run`: smallest one-shot-only surface -- `workspace-full`: explicit advanced opt-in when the chat truly needs shells, services, snapshots, secrets, network policy, or disk tools +## Zero To Hero -## Supported Hosts +1. Validate the host with `pyro doctor`. +2. Pull `debian:12` and prove guest execution with `pyro run debian:12 -- git --version`. +3. Connect Claude Code, Codex, or OpenCode with `pyro mcp serve`. +4. Start with one recipe from [docs/use-cases/README.md](docs/use-cases/README.md). + `repro-fix-loop` is the shortest chat-first story. +5. Use `make smoke-use-cases` as the trustworthy guest-backed verification path + for the advertised workflows. -Supported today: +That is the intended user journey. The terminal commands exist to validate and +debug that chat-host path, not to replace it as the main product story. -- Linux x86_64 -- Python 3.12+ -- `uv` -- `/dev/kvm` +## Manual Terminal Workspace Flow -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 +If you want to understand what the agent gets inside the sandbox, or debug a +recipe outside the chat host, use the terminal companion flow below: ```bash -uvx --from pyro-mcp pyro doctor +uv tool install pyro-mcp +WORKSPACE_ID="$(pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 --id-only)" +pyro workspace list +pyro workspace sync push "$WORKSPACE_ID" ./changes +pyro workspace file read "$WORKSPACE_ID" note.txt --content-only +pyro workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch +pyro workspace exec "$WORKSPACE_ID" -- cat note.txt +pyro workspace snapshot create "$WORKSPACE_ID" checkpoint +pyro workspace reset "$WORKSPACE_ID" --snapshot checkpoint +pyro workspace export "$WORKSPACE_ID" note.txt --output ./note.txt +pyro workspace delete "$WORKSPACE_ID" ``` -Expected success signals: +Add `workspace-full` only when the chat or your manual debugging loop really +needs: -```bash -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 -``` +- persistent PTY shells +- long-running services and readiness probes +- guest networking and published ports +- secrets +- stopped-workspace disk inspection -### 2. Inspect the catalog - -```bash -uvx --from pyro-mcp pyro env list -``` - -Expected output: - -```bash -Catalog version: 4.0.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 - -```bash -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](docs/host-requirements.md) for the full host requirements. - -### 4. Run one command in a guest - -```bash -uvx --from pyro-mcp pyro run debian:12 -- git --version -``` - -Expected success signals: - -```bash -[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 - -```bash -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: - -```json -{ - "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](docs/first-run.md). -The walkthrough GIF above was rendered from [docs/assets/first-run.tape](docs/assets/first-run.tape) using [scripts/render_tape.sh](scripts/render_tape.sh). - -## Stable 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](docs/vision.md) for the product thesis and the -longer-term interaction model. - -```bash -pyro workspace create debian:12 --seed-path ./repo -pyro workspace create debian:12 --network-policy egress -pyro workspace create debian:12 --seed-path ./repo --secret API_TOKEN=expected -pyro workspace create debian:12 --network-policy egress+published-ports -pyro workspace sync push WORKSPACE_ID ./changes --dest src -pyro workspace file list WORKSPACE_ID src --recursive -pyro workspace file read WORKSPACE_ID src/note.txt --content-only -pyro workspace file write WORKSPACE_ID src/app.py --text-file ./app.py -pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch -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 --id-only -pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd' -pyro workspace shell read WORKSPACE_ID SHELL_ID --plain --wait-for-idle-ms 300 -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 start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app -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 stop WORKSPACE_ID -pyro workspace disk list WORKSPACE_ID -pyro workspace disk read WORKSPACE_ID src/note.txt --content-only -pyro workspace disk export WORKSPACE_ID --output ./workspace.ext4 -pyro workspace start WORKSPACE_ID -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, use `--id-only` for only the identifier or `--json` for the full -workspace payload. 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 `4.0.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. Prefer -`pyro workspace shell read --plain --wait-for-idle-ms 300` for chat-facing shell reads. 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 `--network-policy egress` when the workspace needs outbound guest networking, and -`--network-policy egress+published-ports` plus `workspace service start --publish` when one -service must be probed from the host on `127.0.0.1`. -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/`, 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. Use `pyro workspace stop` -plus `pyro workspace disk list|read|export` when you need offline inspection or one raw ext4 copy -from a stopped guest-backed workspace, then `pyro workspace start` to resume the same workspace. - -## 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. +The five recipe docs show when those capabilities are justified: +[docs/use-cases/README.md](docs/use-cases/README.md) ## Official Environments @@ -415,216 +194,10 @@ Current official environments in the shipped catalog: - `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: - -```bash -pyro env list -``` - -Prefetch one environment: - -```bash -pyro env pull debian:12 -``` - -Run one command in an ephemeral VM: - -```bash -pyro run debian:12 -- git --version -``` - -Run with outbound internet enabled: - -```bash -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: - -```bash -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. Start most chat hosts with -`workspace-core`: - -```bash -pyro mcp serve -``` - -Profile progression for chat hosts: - -- `workspace-core`: recommended first profile for normal persistent chat editing -- `vm-run`: expose only `vm_run` for one-shot-only hosts -- `workspace-full`: expose shells, services, snapshots, secrets, network policy, and disk tools when the chat truly needs the full stable surface - -Run the deterministic demo: - -```bash -pyro demo -pyro demo --network -``` - -Run the Ollama demo: - -```bash -ollama serve -ollama pull llama3.2:3b -pyro demo ollama -``` - -## Python SDK - -```python -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: - -```python -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: - -```python -from pyro_mcp import Pyro - -pyro = Pyro() -print(pyro.list_environments()) -print(pyro.inspect_environment("debian:12")) -``` - -For repeated commands in one workspace: - -```python -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_policy="off", 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, published_ports=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, plain=False, wait_for_idle_ms=None)` -- `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)` - -Recommended MCP tool profiles: - -- `vm-run`: `vm_run` only -- `workspace-core`: `vm_run`, `workspace_create`, `workspace_list`, `workspace_update`, `workspace_status`, `workspace_sync_push`, `workspace_exec`, `workspace_logs`, `workspace_file_list`, `workspace_file_read`, `workspace_file_write`, `workspace_patch_apply`, `workspace_diff`, `workspace_export`, `workspace_reset`, `workspace_delete` -- `workspace-full`: the complete stable MCP surface above - -## Integration Examples - -- Python one-shot SDK example: [examples/python_run.py](examples/python_run.py) -- Python lifecycle example: [examples/python_lifecycle.py](examples/python_lifecycle.py) -- Python workspace example: [examples/python_workspace.py](examples/python_workspace.py) -- Claude Code MCP setup: [examples/claude_code_mcp.md](examples/claude_code_mcp.md) -- Codex MCP setup: [examples/codex_mcp.md](examples/codex_mcp.md) -- OpenCode MCP config: [examples/opencode_mcp_config.json](examples/opencode_mcp_config.json) -- Generic MCP client config: [examples/mcp_client_config.md](examples/mcp_client_config.md) -- Claude Desktop MCP config: [examples/claude_desktop_mcp_config.json](examples/claude_desktop_mcp_config.json) -- Cursor MCP config: [examples/cursor_mcp_config.json](examples/cursor_mcp_config.json) -- OpenAI Responses API example: [examples/openai_responses_vm_run.py](examples/openai_responses_vm_run.py) -- OpenAI Responses `workspace-core` example: [examples/openai_responses_workspace_core.py](examples/openai_responses_workspace_core.py) -- LangChain wrapper example: [examples/langchain_vm_run.py](examples/langchain_vm_run.py) -- Agent-ready `vm_run` example: [examples/agent_vm_run.py](examples/agent_vm_run.py) - -## 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. +The embedded Firecracker runtime ships with the package. Official environments +are pulled as OCI artifacts from public Docker Hub into a local cache on first +use or through `pyro env pull`. End users do not need registry credentials to +pull or run the official environments. ## Contributor Workflow @@ -637,11 +210,12 @@ 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. +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: @@ -652,20 +226,9 @@ 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: ```bash 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. diff --git a/docs/first-run.md b/docs/first-run.md index 842bb59..4ac064f 100644 --- a/docs/first-run.md +++ b/docs/first-run.md @@ -1,10 +1,15 @@ # First Run Transcript -This is the intended evaluator path for a first successful run on a supported host. +This is the intended evaluator-to-chat-host path for a first successful run on +a supported host. + Copy the commands as-is. Paths and timing values will differ on your machine. The same sequence works with an installed `pyro` binary by dropping the -`uvx --from pyro-mcp` prefix. If you are running from a source checkout instead -of the published package, replace `pyro` with `uv run pyro`. +`uvx --from pyro-mcp` prefix. If you are running from a source checkout +instead of the published package, replace `pyro` with `uv run pyro`. + +`pyro-mcp` currently has no users. Expect breaking changes while the chat-host +path is still being shaped. ## 1. Verify the host @@ -30,9 +35,10 @@ debian:12-build [installed|not installed] Debian 12 environment with Git and com ## 3. Pull the default environment -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 -[host-requirements.md](host-requirements.md) for the full host requirements. +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 [host-requirements.md](host-requirements.md) for the +full host requirements. ```bash $ uvx --from pyro-mcp pyro env pull debian:12 @@ -45,9 +51,6 @@ Installed: yes Cache dir: /home/you/.cache/pyro-mcp/environments Default packages: bash, coreutils, git Install dir: /home/you/.cache/pyro-mcp/environments/linux-x86_64/debian_12-1.0.0 -Install manifest: /home/you/.cache/pyro-mcp/environments/linux-x86_64/debian_12-1.0.0/environment.json -Kernel image: /home/you/.cache/pyro-mcp/environments/linux-x86_64/debian_12-1.0.0/vmlinux -Rootfs image: /home/you/.cache/pyro-mcp/environments/linux-x86_64/debian_12-1.0.0/rootfs.ext4 OCI source: registry-1.docker.io/thalesmaciel/pyro-environment-debian-12:1.0.0 ``` @@ -62,239 +65,90 @@ $ uvx --from pyro-mcp pyro run debian:12 -- git --version 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. +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. Continue into the stable workspace path +## 5. Start the MCP server -The commands below use the published-package form. The same stable workspace path works with an -installed `pyro` binary by dropping the `uvx --from pyro-mcp` prefix, or with `uv run pyro` from -a source checkout. +Bare `pyro mcp serve` now starts `workspace-core`, which is the intended chat +path: + +```bash +$ uvx --from pyro-mcp pyro mcp serve +``` + +## 6. Connect a chat host + +Claude Code: + +```bash +$ claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve +$ claude mcp list +``` + +Codex: + +```bash +$ codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve +$ codex mcp list +``` + +OpenCode uses the local config shape shown in: + +- [opencode_mcp_config.json](../examples/opencode_mcp_config.json) + +Other host-specific references: + +- [claude_code_mcp.md](../examples/claude_code_mcp.md) +- [codex_mcp.md](../examples/codex_mcp.md) +- [mcp_client_config.md](../examples/mcp_client_config.md) + +## 7. Continue into a real workflow + +Once the host is connected, move to one of the five recipe docs in +[use-cases/README.md](use-cases/README.md). + +The shortest chat-first story is: + +- [use-cases/repro-fix-loop.md](use-cases/repro-fix-loop.md) + +If you want terminal-level visibility into what the agent gets, use the manual +workspace flow below: ```bash $ export WORKSPACE_ID="$(uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 --id-only)" $ uvx --from pyro-mcp pyro workspace list -$ uvx --from pyro-mcp pyro workspace update "$WORKSPACE_ID" --label owner=codex $ uvx --from pyro-mcp pyro workspace sync push "$WORKSPACE_ID" ./changes $ uvx --from pyro-mcp pyro workspace file read "$WORKSPACE_ID" note.txt --content-only $ uvx --from pyro-mcp pyro workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch $ uvx --from pyro-mcp pyro workspace exec "$WORKSPACE_ID" -- cat note.txt $ uvx --from pyro-mcp pyro workspace snapshot create "$WORKSPACE_ID" checkpoint -$ uvx --from pyro-mcp pyro workspace service start "$WORKSPACE_ID" web --ready-file .web-ready -- sh -lc 'touch .web-ready && while true; do sleep 60; done' $ uvx --from pyro-mcp pyro workspace reset "$WORKSPACE_ID" --snapshot checkpoint $ uvx --from pyro-mcp pyro workspace export "$WORKSPACE_ID" note.txt --output ./note.txt -$ uvx --from pyro-mcp pyro workspace stop "$WORKSPACE_ID" -$ uvx --from pyro-mcp pyro workspace disk list "$WORKSPACE_ID" -$ uvx --from pyro-mcp pyro workspace disk read "$WORKSPACE_ID" note.txt --content-only -$ uvx --from pyro-mcp pyro workspace disk export "$WORKSPACE_ID" --output ./workspace.ext4 -$ uvx --from pyro-mcp pyro workspace start "$WORKSPACE_ID" $ uvx --from pyro-mcp pyro workspace delete "$WORKSPACE_ID" ``` -## 6. Optional one-shot demo and expanded workspace flow +Move to `--profile workspace-full` only when the chat really needs shells, +services, snapshots, secrets, network policy, or disk tools. + +## 8. Trust the smoke pack + +The repo now treats the full smoke pack as the trustworthy guest-backed +verification path for the advertised workflows: + +```bash +$ make smoke-use-cases +``` + +That runner creates real guest-backed workspaces, exercises all five documented +stories, exports concrete results where relevant, and cleans up on both success +and failure. + +## 9. Optional one-shot demo ```bash $ uvx --from pyro-mcp pyro demo -$ uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 -$ uvx --from pyro-mcp pyro workspace list -$ uvx --from pyro-mcp pyro workspace update WORKSPACE_ID --label owner=codex -$ uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes -$ uvx --from pyro-mcp pyro workspace file list WORKSPACE_ID src --recursive -$ uvx --from pyro-mcp pyro workspace file read WORKSPACE_ID src/app.py --content-only -$ uvx --from pyro-mcp pyro workspace file write WORKSPACE_ID src/app.py --text-file ./app.py -$ uvx --from pyro-mcp pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch -$ uvx --from pyro-mcp pyro workspace create debian:12 --network-policy egress -$ uvx --from pyro-mcp pyro workspace create debian:12 --secret API_TOKEN=expected --secret-file PIP_TOKEN=./token.txt -$ uvx --from pyro-mcp pyro workspace exec WORKSPACE_ID --secret-env API_TOKEN -- sh -lc 'test "$API_TOKEN" = "expected"' -$ uvx --from pyro-mcp pyro workspace diff WORKSPACE_ID -$ uvx --from pyro-mcp pyro workspace snapshot create WORKSPACE_ID checkpoint -$ uvx --from pyro-mcp pyro workspace reset WORKSPACE_ID --snapshot checkpoint -$ uvx --from pyro-mcp pyro workspace export WORKSPACE_ID note.txt --output ./note.txt -$ uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID --secret-env API_TOKEN --id-only -$ uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID app --secret-env API_TOKEN --ready-file .ready -- sh -lc 'touch .ready && while true; do sleep 60; done' -$ uvx --from pyro-mcp pyro workspace create debian:12 --network-policy egress+published-ports -$ uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app -$ uvx --from pyro-mcp pyro mcp serve -$ claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve -$ codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve -``` - -For most chat hosts, bare `pyro mcp serve` now starts `workspace-core`, the -recommended first MCP profile. -Move to `workspace-full` only when the host truly needs shells, services, -snapshots, secrets, network policy, or disk tools. - -Host-specific MCP starts: - -- Claude Code: [examples/claude_code_mcp.md](../examples/claude_code_mcp.md) -- Codex: [examples/codex_mcp.md](../examples/codex_mcp.md) -- OpenCode: [examples/opencode_mcp_config.json](../examples/opencode_mcp_config.json) -- Generic MCP config: [examples/mcp_client_config.md](../examples/mcp_client_config.md) - -`pyro demo` proves the one-shot create/start/exec/delete VM lifecycle works end to end. - -Once that stable workspace flow works, continue with the five recipe docs in -[use-cases/README.md](use-cases/README.md) or run the real guest-backed smoke packs directly with -`make smoke-use-cases`. Treat that smoke pack as the trustworthy guest-backed -verification path for the advertised workspace workflows. - -When you need repeated commands in one sandbox, switch to `pyro workspace ...`: - -```bash -$ uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo -Workspace ID: ... -Environment: debian:12 -State: started -Workspace: /workspace -Workspace seed: directory from ... -Network policy: off -Execution mode: guest_vsock -Resources: 1 vCPU / 1024 MiB -Command count: 0 - -$ uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes --dest src -[workspace-sync] workspace_id=... mode=directory source=... destination=/workspace/src entry_count=... bytes_written=... execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace file list WORKSPACE_ID src --recursive -Workspace file path: /workspace/src -- /workspace/src/note.txt [file] bytes=... - -$ uvx --from pyro-mcp pyro workspace file read WORKSPACE_ID src/note.txt -hello from synced workspace -[workspace-file-read] workspace_id=... path=/workspace/src/note.txt size_bytes=... truncated=False execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch - -$ uvx --from pyro-mcp pyro workspace exec WORKSPACE_ID -- cat src/note.txt -hello from synced workspace -[workspace-exec] workspace_id=... sequence=1 cwd=/workspace execution_mode=guest_vsock exit_code=0 duration_ms=... - -$ uvx --from pyro-mcp pyro workspace exec WORKSPACE_ID --secret-env API_TOKEN -- sh -lc 'test "$API_TOKEN" = "expected"' -[workspace-exec] workspace_id=... sequence=2 cwd=/workspace execution_mode=guest_vsock exit_code=0 duration_ms=... - -$ uvx --from pyro-mcp pyro workspace diff WORKSPACE_ID -[workspace-diff] workspace_id=... total=... added=... modified=... deleted=... type_changed=... text_patched=... non_text=... ---- a/src/note.txt -+++ b/src/note.txt -@@ ... - -$ uvx --from pyro-mcp pyro workspace snapshot create WORKSPACE_ID checkpoint -[workspace-snapshot-create] snapshot_name=checkpoint kind=named entry_count=... bytes_written=... - -$ uvx --from pyro-mcp pyro workspace reset WORKSPACE_ID --snapshot checkpoint -Workspace reset from snapshot: checkpoint (named) -[workspace-reset] destination=/workspace entry_count=... bytes_written=... -Workspace ID: ... -State: started -Command count: 0 -Reset count: 1 - -$ uvx --from pyro-mcp pyro workspace export WORKSPACE_ID src/note.txt --output ./note.txt -[workspace-export] workspace_id=... workspace_path=/workspace/src/note.txt output_path=... artifact_type=file entry_count=... bytes_written=... execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID --secret-env API_TOKEN --id-only -[workspace-shell-open] workspace_id=... shell_id=... state=running cwd=/workspace cols=120 rows=30 execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd' -[workspace-shell-write] workspace_id=... shell_id=... state=running cwd=/workspace cols=120 rows=30 execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace shell read WORKSPACE_ID SHELL_ID --plain --wait-for-idle-ms 300 -/workspace -[workspace-shell-read] workspace_id=... shell_id=... state=running cursor=0 next_cursor=... truncated=False plain=True wait_for_idle_ms=300 execution_mode=guest_vsock - -$ uvx --from pyro-mcp 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' -[workspace-service-start] workspace_id=... service=web state=running cwd=/workspace ready_type=file execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID worker --ready-file .worker-ready -- sh -lc 'touch .worker-ready && while true; do sleep 60; done' -[workspace-service-start] workspace_id=... service=worker state=running cwd=/workspace ready_type=file execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace create debian:12 --network-policy egress+published-ports -Workspace ID: ... -Network policy: egress+published-ports -... - -$ uvx --from pyro-mcp pyro workspace service start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app -[workspace-service-start] workspace_id=... service=app state=running cwd=/workspace ready_type=http execution_mode=guest_vsock published=127.0.0.1:18080->8080/tcp - -$ uvx --from pyro-mcp pyro workspace service list WORKSPACE_ID -Workspace: ... -Services: 2 total, 2 running -- web [running] cwd=/workspace readiness=file -- worker [running] cwd=/workspace readiness=file - -$ uvx --from pyro-mcp pyro workspace service status WORKSPACE_ID web -Workspace: ... -Service: web -State: running -Command: sh -lc 'touch .web-ready && while true; do sleep 60; done' -Cwd: /workspace -Readiness: file /workspace/.web-ready -Execution mode: guest_vsock - -$ uvx --from pyro-mcp pyro workspace service logs WORKSPACE_ID web --tail-lines 50 -Workspace: ... -Service: web -State: running -... - -$ uvx --from pyro-mcp pyro workspace service stop WORKSPACE_ID web -[workspace-service-stop] workspace_id=... service=web state=stopped execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace service stop WORKSPACE_ID worker -[workspace-service-stop] workspace_id=... service=worker state=stopped execution_mode=guest_vsock - -$ uvx --from pyro-mcp pyro workspace stop WORKSPACE_ID -Workspace ID: ... -State: stopped - -$ uvx --from pyro-mcp pyro workspace disk list WORKSPACE_ID src --recursive -Workspace: ... -Path: /workspace/src -- /workspace/src [directory] -- /workspace/src/note.txt [file] bytes=... - -$ uvx --from pyro-mcp pyro workspace disk read WORKSPACE_ID src/note.txt -hello from synced workspace -[workspace-disk-read] workspace_id=... path=/workspace/src/note.txt size_bytes=... truncated=False - -$ uvx --from pyro-mcp pyro workspace disk export WORKSPACE_ID --output ./workspace.ext4 -[workspace-disk-export] workspace_id=... output_path=... disk_format=ext4 bytes_written=... - -$ uvx --from pyro-mcp pyro workspace start WORKSPACE_ID -Workspace ID: ... -State: started -``` - -Use `--seed-path` when the workspace should 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 need to import later host-side changes into a started -workspace. Sync is non-atomic in `4.0.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 current -`/workspace` tree to its immutable create-time baseline, `pyro workspace snapshot *` to create -named checkpoints, and `pyro workspace export` to copy one changed file or directory back to the -host. Use `pyro workspace file *` and `pyro workspace patch apply` for model-native text edits, -`pyro workspace exec` for one-shot commands, and `pyro workspace shell *` when you -need a persistent interactive PTY session in that same workspace. Use `pyro workspace service *` -when the workspace needs long-running background processes with typed readiness checks. Internal -service state and logs stay outside `/workspace`, so service runtime data does not appear in -workspace diff or export results. Use `--network-policy egress` for outbound guest networking, and -`--network-policy egress+published-ports` plus `workspace service start --publish` when one -service must be reachable from the host on `127.0.0.1`. Use `--secret` and `--secret-file` at -workspace creation when the sandbox needs private tokens or config. Persisted secret files are -materialized at `/run/pyro-secrets/`, and `--secret-env SECRET_NAME[=ENV_VAR]` maps one -secret into one exec, shell, or service call without storing that environment mapping on the -workspace itself. Use `pyro workspace stop` plus `pyro workspace disk list|read|export` when you -need offline inspection or one raw ext4 copy from a stopped guest-backed workspace, then -`pyro workspace start` to resume the same workspace. - -The stable workspace walkthrough GIF in the README is rendered from -[docs/assets/workspace-first-run.tape](assets/workspace-first-run.tape) with -[scripts/render_tape.sh](../scripts/render_tape.sh). - -Example output: - -```json { "cleanup": { "deleted": true, @@ -309,7 +163,5 @@ Example output: } ``` -When you are done evaluating and want to remove stale cached environments, run `pyro env prune`. - -If `pyro doctor` reports `Runtime: FAIL`, or if the `pyro run` summary does not show -`execution_mode=guest_vsock`, stop and use [troubleshooting.md](troubleshooting.md). +`pyro demo` proves the one-shot create/start/exec/delete VM lifecycle works end +to end. diff --git a/docs/install.md b/docs/install.md index 88f3d84..6ef5649 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,11 +1,17 @@ # Install +`pyro-mcp` is built for chat-based coding agents on Linux `x86_64` with KVM. +This document is intentionally biased toward that path. + +`pyro-mcp` currently has no users. Expect breaking changes while the chat-host +flow is still being shaped. + ## Support Matrix Supported today: -- Linux x86_64 -- Python 3.12+ +- Linux `x86_64` +- Python `3.12+` - `uv` - `/dev/kvm` @@ -53,11 +59,12 @@ pyro env pull debian:12 pyro run debian:12 -- git --version ``` -If you are running from a repo checkout instead, replace `pyro` with `uv run pyro`. +If you are running from a repo checkout instead, replace `pyro` with +`uv run pyro`. -After that one-shot proof works, continue into the stable workspace path with `pyro workspace ...`. +After that one-shot proof works, the intended next step is `pyro mcp serve`. -### 1. Check the host first +## 1. Check the host ```bash uvx --from pyro-mcp pyro doctor @@ -76,7 +83,7 @@ Networking: tun=yes ip_forward=yes If `Runtime: FAIL`, stop here and use [troubleshooting.md](troubleshooting.md). -### 2. Inspect the catalog +## 2. Inspect the catalog ```bash uvx --from pyro-mcp pyro env list @@ -91,15 +98,16 @@ debian:12-base [installed|not installed] Minimal Debian 12 environment for shell debian:12-build [installed|not installed] Debian 12 environment with Git and common build tools preinstalled. ``` -### 3. Pull the default environment +## 3. Pull the default environment ```bash 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 -[host-requirements.md](host-requirements.md) for the full host requirements. +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 [host-requirements.md](host-requirements.md) for the +full host requirements. Expected success signals: @@ -110,7 +118,7 @@ Pulled: debian:12 ... ``` -### 4. Run one command in a guest +## 4. Run one command in a guest ```bash uvx --from pyro-mcp pyro run debian:12 -- git --version @@ -126,119 +134,14 @@ Expected success signals: 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 +The guest command output and the `[run] ...` summary are written to different +streams, so they may appear in either order. Use `--json` if you need a deterministic structured result. -If guest execution is unavailable, the command fails unless you explicitly pass -`--allow-host-compat`. +## 5. Connect a chat host -## 5. Continue into the stable workspace path - -The commands below use plain `pyro ...`. Run the same flow with `uvx --from pyro-mcp pyro ...` -for the published package, or `uv run pyro ...` from a source checkout. - -```bash -uv tool install pyro-mcp -WORKSPACE_ID="$(pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 --id-only)" -pyro workspace list -pyro workspace update "$WORKSPACE_ID" --label owner=codex -pyro workspace sync push "$WORKSPACE_ID" ./changes -pyro workspace file read "$WORKSPACE_ID" note.txt --content-only -pyro workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch -pyro workspace exec "$WORKSPACE_ID" -- cat note.txt -pyro workspace snapshot create "$WORKSPACE_ID" checkpoint -pyro workspace service start "$WORKSPACE_ID" web --ready-file .web-ready -- sh -lc 'touch .web-ready && while true; do sleep 60; done' -pyro workspace reset "$WORKSPACE_ID" --snapshot checkpoint -pyro workspace export "$WORKSPACE_ID" note.txt --output ./note.txt -pyro workspace delete "$WORKSPACE_ID" -``` - -This is the stable persistent-workspace contract: - -- `workspace create` seeds `/workspace` -- `workspace create --name/--label`, `workspace list`, and `workspace update` make workspaces discoverable -- `workspace sync push` imports later host-side changes -- `workspace file *` and `workspace patch apply` cover model-native text inspection and edits -- `workspace exec` and `workspace shell *` keep work inside one sandbox -- `workspace service *` manages long-running processes with typed readiness -- `workspace snapshot *` and `workspace reset` make reset-over-repair explicit -- `workspace diff` compares against the immutable create-time baseline -- `workspace export` copies results back to the host -- `workspace stop|start` and `workspace disk *` add secondary stopped-workspace inspection and raw ext4 export - -When that stable workspace path is working, continue with the recipe index at -[use-cases/README.md](use-cases/README.md). It groups the five core workspace stories and the -real smoke targets behind them, starting with `make smoke-use-cases` or one of the per-scenario -targets such as `make smoke-repro-fix-loop`. -Treat `make smoke-use-cases` as the trustworthy guest-backed verification path for the advertised -workspace workflows. - -## 6. Optional demo proof point - -```bash -uvx --from pyro-mcp pyro demo -``` - -`pyro demo` proves the one-shot create/start/exec/delete VM lifecycle works end to end. - -Example output: - -```json -{ - "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" -} -``` - -For a fuller copy-pasteable transcript, see [first-run.md](first-run.md). -When you are done evaluating and want to remove stale cached environments, run `pyro env prune`. - -## Installed CLI - -If you already installed the package, the same evaluator path works with plain `pyro ...`: - -```bash -uv tool install pyro-mcp -pyro --version -pyro doctor -pyro env list -pyro env pull debian:12 -pyro run debian:12 -- git --version -``` - -After the CLI path works, you can move on to: - -- persistent workspaces: `pyro workspace create debian:12 --seed-path ./repo` -- workspace discovery metadata: `pyro workspace create debian:12 --name repro-fix --label issue=123` -- workspace discovery commands: `pyro workspace list` and `pyro workspace update WORKSPACE_ID --label owner=codex` -- live workspace updates: `pyro workspace sync push WORKSPACE_ID ./changes` -- guest networking policy: `pyro workspace create debian:12 --network-policy egress` -- workspace secrets: `pyro workspace create debian:12 --secret API_TOKEN=expected --secret-file PIP_TOKEN=./token.txt` -- model-native file editing: `pyro workspace file read WORKSPACE_ID src/app.py --content-only`, `pyro workspace file write WORKSPACE_ID src/app.py --text-file ./app.py`, and `pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch` -- baseline diff: `pyro workspace diff WORKSPACE_ID` -- snapshots and reset: `pyro workspace snapshot create WORKSPACE_ID checkpoint` and `pyro workspace reset WORKSPACE_ID --snapshot checkpoint` -- host export: `pyro workspace export WORKSPACE_ID note.txt --output ./note.txt` -- stopped-workspace inspection: `pyro workspace stop WORKSPACE_ID`, `pyro workspace disk list WORKSPACE_ID`, `pyro workspace disk read WORKSPACE_ID note.txt --content-only`, and `pyro workspace disk export WORKSPACE_ID --output ./workspace.ext4` -- interactive shells: `pyro workspace shell open WORKSPACE_ID --id-only` -- long-running services: `pyro workspace service start WORKSPACE_ID app --ready-file .ready -- sh -lc 'touch .ready && while true; do sleep 60; done'` -- localhost-published ports: `pyro workspace create debian:12 --network-policy egress+published-ports` and `pyro workspace service start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app` -- MCP: `pyro mcp serve` -- Python SDK: `from pyro_mcp import Pyro` -- Demos: `pyro demo` or `pyro demo --network` - -## Chat Host Quickstart - -For most chat-host integrations, bare `pyro mcp serve` now starts -`workspace-core`: +Bare `pyro mcp serve` now starts `workspace-core`, which is the default +product path. ```bash uvx --from pyro-mcp pyro mcp serve @@ -246,10 +149,10 @@ uvx --from pyro-mcp pyro mcp serve Copy-paste host-specific starts: -- Claude Code: [examples/claude_code_mcp.md](../examples/claude_code_mcp.md) -- Codex: [examples/codex_mcp.md](../examples/codex_mcp.md) -- OpenCode: [examples/opencode_mcp_config.json](../examples/opencode_mcp_config.json) -- Generic MCP config: [examples/mcp_client_config.md](../examples/mcp_client_config.md) +- Claude Code setup: [claude_code_mcp.md](../examples/claude_code_mcp.md) +- Codex setup: [codex_mcp.md](../examples/codex_mcp.md) +- OpenCode config: [opencode_mcp_config.json](../examples/opencode_mcp_config.json) +- Generic MCP fallback: [mcp_client_config.md](../examples/mcp_client_config.md) Claude Code: @@ -263,86 +166,87 @@ Codex: codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve ``` -OpenCode uses the `mcp`/`type: "local"` config shape shown in -[examples/opencode_mcp_config.json](../examples/opencode_mcp_config.json). If -`pyro-mcp` is already installed, replace the `uvx --from pyro-mcp pyro` -command with `pyro` in the same host-specific command or config shape. Use -`--profile workspace-full` only when the host truly needs the full advanced -workspace surface. +OpenCode uses the `mcp` / `type: "local"` config shape shown in +[opencode_mcp_config.json](../examples/opencode_mcp_config.json). -Use profile progression like this: +If `pyro-mcp` is already installed, replace `uvx --from pyro-mcp pyro` with +`pyro` in the same command or config shape. -- `workspace-core`: default and recommended first profile for normal persistent chat editing -- `vm-run`: one-shot-only integrations -- `workspace-full`: explicit advanced opt-in when the host truly needs shells, services, snapshots, secrets, network policy, or disk tools +Use `--profile workspace-full` only when the chat truly needs shells, services, +snapshots, secrets, network policy, or disk tools. -## Stable Workspace +## 6. Go from zero to hero -Use `pyro workspace ...` when you need repeated commands in one sandbox instead of one-shot `pyro run`. +The intended user journey is: + +1. validate the host with `pyro doctor` +2. pull `debian:12` +3. prove guest execution with `pyro run debian:12 -- git --version` +4. connect Claude Code, Codex, or OpenCode with `pyro mcp serve` +5. start with one use-case recipe from [use-cases/README.md](use-cases/README.md) +6. trust but verify with `make smoke-use-cases` + +If you want the shortest chat-first story, start with +[use-cases/repro-fix-loop.md](use-cases/repro-fix-loop.md). + +## 7. Manual terminal workspace flow + +If you want to inspect the workspace model directly from the terminal, use the +companion flow below. This is for understanding and debugging the chat-host +product, not the primary story. ```bash -pyro workspace create debian:12 --seed-path ./repo -pyro workspace create debian:12 --network-policy egress -pyro workspace create debian:12 --seed-path ./repo --secret API_TOKEN=expected -pyro workspace create debian:12 --network-policy egress+published-ports -pyro workspace sync push WORKSPACE_ID ./changes --dest src -pyro workspace file list WORKSPACE_ID src --recursive -pyro workspace file read WORKSPACE_ID src/note.txt --content-only -pyro workspace file write WORKSPACE_ID src/app.py --text-file ./app.py -pyro workspace patch apply WORKSPACE_ID --patch-file fix.patch -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 --id-only -pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd' -pyro workspace shell read WORKSPACE_ID SHELL_ID --plain --wait-for-idle-ms 300 -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 start WORKSPACE_ID app --ready-http http://127.0.0.1:8080/ --publish 18080:8080 -- ./start-app -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 stop WORKSPACE_ID -pyro workspace disk list WORKSPACE_ID -pyro workspace disk read WORKSPACE_ID src/note.txt --content-only -pyro workspace disk export WORKSPACE_ID --output ./workspace.ext4 -pyro workspace start WORKSPACE_ID -pyro workspace logs WORKSPACE_ID -pyro workspace delete WORKSPACE_ID +uv tool install pyro-mcp +WORKSPACE_ID="$(pyro workspace create debian:12 --seed-path ./repo --name repro-fix --label issue=123 --id-only)" +pyro workspace list +pyro workspace update "$WORKSPACE_ID" --label owner=codex +pyro workspace sync push "$WORKSPACE_ID" ./changes +pyro workspace file read "$WORKSPACE_ID" note.txt --content-only +pyro workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch +pyro workspace exec "$WORKSPACE_ID" -- cat note.txt +pyro workspace snapshot create "$WORKSPACE_ID" checkpoint +pyro workspace reset "$WORKSPACE_ID" --snapshot checkpoint +pyro workspace export "$WORKSPACE_ID" note.txt --output ./note.txt +pyro workspace delete "$WORKSPACE_ID" ``` -Workspace commands default to the persistent `/workspace` directory inside the guest. If you need -the identifier programmatically, use `--id-only` for only the identifier or `--json` for the full -workspace payload. Use `--seed-path` -when the workspace should start from a host directory or a local `.tar` / `.tar.gz` / `.tgz` -archive. Use `pyro workspace sync push` for later host-side changes to a started workspace. Sync -is non-atomic in `4.0.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 current workspace -tree to its immutable create-time baseline, `pyro workspace snapshot *` to capture named -checkpoints, and `pyro workspace export` to copy one changed file or directory back to the host. Use -`pyro workspace exec` for one-shot commands and `pyro workspace shell *` when you need an -interactive PTY that survives across separate calls. Prefer -`pyro workspace shell read --plain --wait-for-idle-ms 300` for chat-facing shell loops. Use `pyro workspace service *` when the -workspace needs long-running background processes with typed readiness probes. Service metadata and -logs stay outside `/workspace`, so the service runtime itself does not show up in workspace diff or -export results. Use `--network-policy egress` when the workspace needs outbound guest networking, -and `--network-policy egress+published-ports` plus `workspace service start --publish` when one -service must be reachable from the host on `127.0.0.1`. Use `--secret` and `--secret-file` at -workspace creation when the sandbox needs private tokens or config, and -`--secret-env SECRET_NAME[=ENV_VAR]` when one exec, shell, or service call needs that secret as an -environment variable. Persisted secret files are available in the guest at -`/run/pyro-secrets/`. Use `pyro workspace stop` plus `pyro workspace disk list|read|export` -when you need offline inspection or one raw ext4 copy from a stopped guest-backed workspace, then -`pyro workspace start` to resume it. +When you need deeper debugging or richer recipes, add: -## Contributor Clone +- `pyro workspace shell *` for interactive PTY state +- `pyro workspace service *` for long-running processes and readiness probes +- `pyro workspace create --network-policy egress+published-ports` plus + `workspace service start --publish` for host-probed services +- `pyro workspace create --secret` and `--secret-file` when the sandbox needs + private tokens +- `pyro workspace stop` plus `workspace disk *` for offline inspection + +## 8. Trustworthy verification path + +The five recipe docs in [use-cases/README.md](use-cases/README.md) are backed +by a real Firecracker smoke pack: + +```bash +make smoke-use-cases +``` + +Treat that smoke pack as the trustworthy guest-backed verification path for the +advertised chat-host workflows. + +## Installed CLI + +If you already installed the package, the same path works with plain `pyro ...`: + +```bash +uv tool install pyro-mcp +pyro --version +pyro doctor +pyro env list +pyro env pull debian:12 +pyro run debian:12 -- git --version +pyro mcp serve +``` + +## Contributor clone ```bash git lfs install diff --git a/docs/integrations.md b/docs/integrations.md index 2883784..c3bbcb8 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -1,164 +1,141 @@ -# Integration Targets +# Chat Host Integrations -These are the main ways to integrate `pyro-mcp` into an LLM application. +This page documents the intended product path for `pyro-mcp`: -Use this page after you have already validated the host and guest execution through the -CLI path in [install.md](install.md) or [first-run.md](first-run.md). +- validate the host with the CLI +- run `pyro mcp serve` +- connect a chat host +- let the agent work inside disposable workspaces + +`pyro-mcp` currently has no users. Expect breaking changes while this chat-host +path is still being shaped. + +Use this page after you have already validated the host and guest execution +through [install.md](install.md) or [first-run.md](first-run.md). ## Recommended Default -Bare `pyro mcp serve` now starts `workspace-core`. Use `vm_run` only for one-shot -integrations, and promote the chat surface to `workspace-full` only when it -truly needs shells, services, snapshots, secrets, network policy, or disk -tools. +Bare `pyro mcp serve` starts `workspace-core`. That is the product path. -That keeps the model-facing contract small: +```bash +pyro mcp serve +``` -- one tool -- one command -- one ephemeral VM -- automatic cleanup +Use `--profile workspace-full` only when the chat truly needs shells, services, +snapshots, secrets, network policy, or disk tools. -Profile progression: +## Claude Code -- `workspace-core`: default and recommended first profile for persistent chat editing -- `vm-run`: one-shot only -- `workspace-full`: the full stable workspace surface, including shells, services, snapshots, secrets, network policy, and disk tools +Package without install: -## OpenAI Responses API +```bash +claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve +claude mcp list +``` -Best when: +Already installed: -- your agent already uses OpenAI models directly -- you want a normal tool-calling loop instead of MCP transport -- you want the smallest amount of integration code +```bash +claude mcp add pyro -- pyro mcp serve +claude mcp list +``` -Recommended surface: +Reference: -- `vm_run` for one-shot loops -- the `workspace-core` tool set for the normal persistent chat loop -- the `workspace-full` tool set only when the host explicitly needs advanced workspace capabilities +- [claude_code_mcp.md](../examples/claude_code_mcp.md) -Canonical example: +## Codex -- [examples/openai_responses_vm_run.py](../examples/openai_responses_vm_run.py) -- [examples/openai_responses_workspace_core.py](../examples/openai_responses_workspace_core.py) -- [docs/use-cases/repro-fix-loop.md](use-cases/repro-fix-loop.md) +Package without install: -## MCP Clients +```bash +codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve +codex mcp list +``` -Best when: +Already installed: -- your host application already supports MCP -- you want `pyro` to run as an external stdio server -- you want tool schemas to be discovered directly from the server +```bash +codex mcp add pyro -- pyro mcp serve +codex mcp list +``` -Recommended entrypoint: +Reference: -- `pyro mcp serve` +- [codex_mcp.md](../examples/codex_mcp.md) -Profile progression: +## OpenCode -- `pyro mcp serve --profile vm-run` for the smallest one-shot surface -- `pyro mcp serve` for the normal persistent chat loop -- `pyro mcp serve --profile workspace-full` only when the model truly needs advanced workspace tools +Use the local MCP config shape from: -Host-specific onramps: +- [opencode_mcp_config.json](../examples/opencode_mcp_config.json) -- Claude Code: [examples/claude_code_mcp.md](../examples/claude_code_mcp.md) -- Codex: [examples/codex_mcp.md](../examples/codex_mcp.md) -- OpenCode: [examples/opencode_mcp_config.json](../examples/opencode_mcp_config.json) -- Generic MCP config: [examples/mcp_client_config.md](../examples/mcp_client_config.md) -- Claude Desktop fallback: [examples/claude_desktop_mcp_config.json](../examples/claude_desktop_mcp_config.json) -- Cursor fallback: [examples/cursor_mcp_config.json](../examples/cursor_mcp_config.json) -- Use-case recipes: [docs/use-cases/README.md](use-cases/README.md) +Minimal `opencode.json` snippet: -## Direct Python SDK +```json +{ + "mcp": { + "pyro": { + "type": "local", + "enabled": true, + "command": ["uvx", "--from", "pyro-mcp", "pyro", "mcp", "serve"] + } + } +} +``` -Best when: +If `pyro-mcp` is already installed, replace `uvx --from pyro-mcp pyro` with +`pyro` in the same config shape. -- your application owns orchestration itself -- you do not need MCP transport -- you want direct access to `Pyro` +## Generic MCP Fallback -Recommended default: +Use this only when the host expects a plain `mcpServers` JSON config and does +not already have a dedicated example in the repo: -- `Pyro.run_in_vm(...)` -- `Pyro.create_server()` for most chat hosts now that `workspace-core` is the default profile -- `Pyro.create_workspace(name=..., labels=...)` + `Pyro.list_workspaces()` + `Pyro.update_workspace(...)` when repeated workspaces need human-friendly discovery metadata -- `Pyro.create_workspace(seed_path=...)` + `Pyro.push_workspace_sync(...)` + `Pyro.exec_workspace(...)` when repeated workspace commands are required -- `Pyro.list_workspace_files(...)` / `Pyro.read_workspace_file(...)` / `Pyro.write_workspace_file(...)` / `Pyro.apply_workspace_patch(...)` when the agent needs model-native file inspection and text edits inside one live workspace -- `Pyro.create_workspace(..., secrets=...)` + `Pyro.exec_workspace(..., secret_env=...)` when the workspace needs private tokens or authenticated setup -- `Pyro.create_workspace(..., network_policy="egress+published-ports")` + `Pyro.start_service(..., published_ports=[...])` when the host must probe one workspace service -- `Pyro.diff_workspace(...)` + `Pyro.export_workspace(...)` when the agent needs baseline comparison or host-out file transfer -- `Pyro.start_service(..., secret_env=...)` + `Pyro.list_services(...)` + `Pyro.logs_service(...)` when the agent needs long-running background processes in one workspace -- `Pyro.open_shell(..., secret_env=...)` + `Pyro.write_shell(...)` + `Pyro.read_shell(..., plain=True, wait_for_idle_ms=300)` when the agent needs an interactive PTY inside the workspace +- [mcp_client_config.md](../examples/mcp_client_config.md) -Lifecycle note: +Generic `mcpServers` shape: -- `Pyro.exec_vm(...)` runs one command and auto-cleans the VM afterward -- use `create_vm(...)` + `start_vm(...)` only when you need pre-exec inspection or status before - that final exec -- use `create_workspace(seed_path=...)` when the agent needs repeated commands in one persistent - `/workspace` that starts from host content -- use `create_workspace(name=..., labels=...)`, `list_workspaces()`, and `update_workspace(...)` - when the agent or operator needs to rediscover the right workspace later without external notes -- use `push_workspace_sync(...)` when later host-side changes need to be imported into that - running workspace without recreating it -- use `list_workspace_files(...)`, `read_workspace_file(...)`, `write_workspace_file(...)`, and - `apply_workspace_patch(...)` when the agent should inspect or edit workspace files without shell - quoting tricks -- use `create_workspace(..., secrets=...)` plus `secret_env` on exec, shell, or service start when - the agent needs private tokens or authenticated startup inside that workspace -- use `create_workspace(..., network_policy="egress+published-ports")` plus - `start_service(..., published_ports=[...])` when the host must probe one service from that - workspace -- use `diff_workspace(...)` when the agent needs a structured comparison against the immutable - create-time baseline -- use `export_workspace(...)` when the agent needs one file or directory copied back to the host -- use `stop_workspace(...)` plus `list_workspace_disk(...)`, `read_workspace_disk(...)`, or - `export_workspace_disk(...)` when the agent needs offline inspection or one raw ext4 copy from - a stopped guest-backed workspace -- use `start_service(...)` when the agent needs long-running processes and typed readiness inside - one workspace -- use `open_shell(...)` when the agent needs interactive shell state instead of one-shot execs +```json +{ + "mcpServers": { + "pyro": { + "command": "uvx", + "args": ["--from", "pyro-mcp", "pyro", "mcp", "serve"] + } + } +} +``` -Examples: +## When To Use `workspace-full` -- [examples/python_run.py](../examples/python_run.py) -- [examples/python_lifecycle.py](../examples/python_lifecycle.py) -- [examples/python_workspace.py](../examples/python_workspace.py) -- [examples/python_shell.py](../examples/python_shell.py) -- [docs/use-cases/README.md](use-cases/README.md) +Stay on bare `pyro mcp serve` unless the chat host truly needs: -## Agent Framework Wrappers +- persistent PTY shell sessions +- long-running services and readiness probes +- secrets +- guest networking and published ports +- stopped-workspace disk inspection or raw ext4 export -Examples: +When that is necessary: -- LangChain tools -- PydanticAI tools -- custom in-house orchestration layers +```bash +pyro mcp serve --profile workspace-full +``` -Best when: +## Recipe-Backed Workflows -- you already have an application framework that expects a Python callable tool -- you want to wrap `vm_run` behind framework-specific abstractions +Once the host is connected, move to the five real workflows in +[use-cases/README.md](use-cases/README.md): -Recommended pattern: +- cold-start repo validation +- repro plus fix loops +- parallel isolated workspaces +- unsafe or untrusted code inspection +- review and evaluation workflows -- keep the framework wrapper thin -- map one-shot framework tool input directly onto `vm_run` -- expose `workspace_*` only when the framework truly needs repeated commands in one workspace +Validate the whole story with: -Concrete example: - -- [examples/langchain_vm_run.py](../examples/langchain_vm_run.py) - -## Selection Rule - -Choose the narrowest integration that matches the host environment: - -1. OpenAI Responses API if you want a direct provider tool loop. -2. MCP if your host already speaks MCP. -3. Python SDK if you own orchestration and do not need transport. -4. Framework wrappers only as thin adapters over the same `vm_run` contract. +```bash +make smoke-use-cases +``` diff --git a/docs/public-contract.md b/docs/public-contract.md index d225c0b..0a4de23 100644 --- a/docs/public-contract.md +++ b/docs/public-contract.md @@ -1,375 +1,150 @@ # Public Contract -This document defines the stable public interface for `pyro-mcp` `3.x`. +This document describes the chat way to use `pyro-mcp` in `4.x`. + +`pyro-mcp` currently has no users. Expect breaking changes while this chat-host +path is still being shaped. + +This document is intentionally biased. It describes the path users are meant to +follow today: + +- prove the host with the terminal companion commands +- serve disposable workspaces over MCP +- connect Claude Code, Codex, or OpenCode +- use the recipe-backed workflows + +This page does not try to document every building block in the repo. It +documents the chat-host path the project is actively shaping. ## Package Identity -- Distribution name: `pyro-mcp` -- Public executable: `pyro` -- Public Python import: `from pyro_mcp import Pyro` -- Public package-level factory: `from pyro_mcp import create_server` +- distribution name: `pyro-mcp` +- public executable: `pyro` +- primary product entrypoint: `pyro mcp serve` -Stable product framing: +`pyro-mcp` is a disposable MCP workspace for chat-based coding agents on Linux +`x86_64` KVM hosts. -- `pyro run` is the stable one-shot entrypoint. -- `pyro workspace ...` is the stable persistent workspace contract. +## Supported Product Path -## CLI Contract +The intended user journey is: -Top-level commands: +1. `pyro doctor` +2. `pyro env list` +3. `pyro env pull debian:12` +4. `pyro run debian:12 -- git --version` +5. `pyro mcp serve` +6. connect Claude Code, Codex, or OpenCode +7. run one of the documented recipe-backed workflows +8. validate the whole story with `make smoke-use-cases` +## Evaluator CLI + +These terminal commands are the documented companion path for the chat-host +product: + +- `pyro doctor` - `pyro env list` - `pyro env pull` -- `pyro env inspect` -- `pyro env prune` -- `pyro mcp serve` - `pyro run` -- `pyro workspace create` -- `pyro workspace list` -- `pyro workspace sync push` -- `pyro workspace stop` -- `pyro workspace start` -- `pyro workspace exec` -- `pyro workspace file list` -- `pyro workspace file read` -- `pyro workspace file write` -- `pyro workspace export` -- `pyro workspace patch apply` -- `pyro workspace disk export` -- `pyro workspace disk list` -- `pyro workspace disk read` -- `pyro workspace diff` -- `pyro workspace snapshot create` -- `pyro workspace snapshot list` -- `pyro workspace snapshot delete` -- `pyro workspace reset` -- `pyro workspace service start` -- `pyro workspace service list` -- `pyro workspace service status` -- `pyro workspace service logs` -- `pyro workspace service stop` -- `pyro workspace shell open` -- `pyro workspace shell read` -- `pyro workspace shell write` -- `pyro workspace shell signal` -- `pyro workspace shell close` -- `pyro workspace status` -- `pyro workspace update` -- `pyro workspace logs` -- `pyro workspace delete` -- `pyro doctor` - `pyro demo` -- `pyro demo ollama` -Stable `pyro run` interface: +What to expect from that path: -- positional environment name -- `--vcpu-count` -- `--mem-mib` -- `--timeout-seconds` -- `--ttl-seconds` -- `--network` -- `--allow-host-compat` -- `--json` +- `pyro run -- ` defaults to `1 vCPU / 1024 MiB` +- `pyro run` fails if guest boot or guest exec is unavailable unless + `--allow-host-compat` is set +- `pyro run`, `pyro env list`, `pyro env pull`, `pyro env inspect`, + `pyro env prune`, and `pyro doctor` are human-readable by default and return + structured JSON with `--json` +- the first official environment pull downloads from public Docker Hub into the + local environment cache +- `pyro demo` proves the one-shot create/start/exec/delete VM lifecycle end to + end -Behavioral guarantees: +These commands exist to validate and debug the chat-host path. They are not the +main product destination. -- `pyro run -- ` defaults to `1 vCPU / 1024 MiB`. -- `pyro run` fails if guest boot or guest exec is unavailable unless `--allow-host-compat` is set. -- `pyro run`, `pyro env list`, `pyro env pull`, `pyro env inspect`, `pyro env prune`, and `pyro doctor` are human-readable by default and return structured JSON with `--json`. -- `pyro demo ollama` prints log lines plus a final summary line. -- `pyro workspace create` auto-starts a persistent workspace. -- `pyro workspace create --seed-path PATH` seeds `/workspace` from a host directory or a local `.tar` / `.tar.gz` / `.tgz` archive before the workspace is returned. -- `pyro workspace create --id-only` prints only the new `workspace_id` plus a trailing newline. -- `pyro workspace create --name NAME --label KEY=VALUE` attaches human-oriented discovery metadata without changing the stable `workspace_id`. -- `pyro workspace create --network-policy {off,egress,egress+published-ports}` controls workspace guest networking and whether services may publish localhost ports. -- `pyro mcp serve --profile {vm-run,workspace-core,workspace-full}` narrows the model-facing MCP surface without changing runtime behavior; `workspace-core` is the recommended first profile for most chat hosts. -- `pyro workspace create --secret NAME=VALUE` and `--secret-file NAME=PATH` persist guest-only UTF-8 secrets outside `/workspace`. -- `pyro workspace list` returns persisted workspaces sorted by most recent `last_activity_at`. -- `pyro workspace sync push WORKSPACE_ID SOURCE_PATH [--dest WORKSPACE_PATH]` imports later host-side directory or archive content into a started workspace. -- `pyro workspace stop WORKSPACE_ID` stops one persistent workspace without deleting its `/workspace`, snapshots, or command history. -- `pyro workspace start WORKSPACE_ID` restarts one stopped workspace without resetting `/workspace`. -- `pyro workspace file list WORKSPACE_ID [PATH] [--recursive]` returns metadata for one live path under `/workspace`. -- `pyro workspace file read WORKSPACE_ID PATH [--max-bytes N] [--content-only]` reads one regular text file under `/workspace`. -- `pyro workspace file write WORKSPACE_ID PATH --text TEXT` and `--text-file PATH` create or replace one regular text file under `/workspace`, creating missing parent directories automatically. -- `pyro workspace export WORKSPACE_ID PATH --output HOST_PATH` exports one file or directory from `/workspace` back to the host. -- `pyro workspace disk export WORKSPACE_ID --output HOST_PATH` copies the stopped guest-backed workspace rootfs as raw ext4 to the host. -- `pyro workspace disk list WORKSPACE_ID [PATH] [--recursive]` inspects a stopped guest-backed workspace rootfs offline without booting the guest. -- `pyro workspace disk read WORKSPACE_ID PATH [--max-bytes N] [--content-only]` reads one regular file from a stopped guest-backed workspace rootfs offline. -- `pyro workspace disk *` requires `state=stopped` and a guest-backed workspace; it fails on `host_compat`. -- `pyro workspace diff WORKSPACE_ID` compares the current `/workspace` tree to the immutable create-time baseline. -- `pyro workspace snapshot *` manages explicit named snapshots in addition to the implicit `baseline`. -- `pyro workspace reset WORKSPACE_ID [--snapshot SNAPSHOT_NAME|baseline]` recreates the full sandbox and restores `/workspace` from the chosen snapshot. -- `pyro workspace service *` manages long-running named services inside one started workspace with typed readiness probes. -- `pyro workspace service start --publish GUEST_PORT` or `--publish HOST_PORT:GUEST_PORT` publishes one guest TCP port to `127.0.0.1` on the host. -- `pyro workspace exec --secret-env SECRET_NAME[=ENV_VAR]` maps one persisted secret into one exec call. -- `pyro workspace service start --secret-env SECRET_NAME[=ENV_VAR]` maps one persisted secret into one service start call. -- `pyro workspace exec` runs in the persistent `/workspace` for that workspace and does not auto-clean. -- `pyro workspace patch apply WORKSPACE_ID --patch TEXT` and `--patch-file PATH` apply one unified text patch with add/modify/delete operations under `/workspace`. -- `pyro workspace shell open --id-only` prints only the new `shell_id` plus a trailing newline. -- `pyro workspace shell open --secret-env SECRET_NAME[=ENV_VAR]` maps one persisted secret into the opened shell environment. -- `pyro workspace shell *` manages persistent PTY sessions inside a started workspace. -- `pyro workspace shell read --plain --wait-for-idle-ms 300` is the recommended chat-facing read mode; raw shell reads remain available without `--plain`. -- `pyro workspace logs` returns persisted command history for that workspace until `pyro workspace delete`. -- `pyro workspace update` changes only discovery metadata such as `name` and key/value `labels`. -- Workspace create/status results expose `workspace_seed` metadata describing how `/workspace` was initialized. -- Workspace create/status/reset/update results expose `name`, `labels`, and `last_activity_at`. -- Workspace create/status/reset results expose `network_policy`. -- Workspace create/status/reset results expose `reset_count` and `last_reset_at`. -- Workspace create/status/reset results expose safe `secrets` metadata with each secret name and source kind, but never the secret values. -- `pyro workspace status` includes aggregate `service_count` and `running_service_count` fields. -- `pyro workspace list` returns one summary row per persisted workspace with `workspace_id`, `name`, `labels`, `environment`, `state`, `created_at`, `last_activity_at`, `expires_at`, `command_count`, `service_count`, and `running_service_count`. -- `pyro workspace service start`, `pyro workspace service list`, and `pyro workspace service status` expose published-port metadata when present. +## MCP Entry Point -## Python SDK Contract +The product entrypoint is: -Primary facade: +```bash +pyro mcp serve +``` -- `Pyro` +What to expect: -Supported public entrypoints: +- bare `pyro mcp serve` starts `workspace-core` +- `workspace-core` is the default product path for chat hosts +- `pyro mcp serve --profile workspace-full` explicitly opts into the larger + tool surface +- `pyro mcp serve --profile vm-run` exposes the smallest one-shot-only surface -- `create_server()` -- `Pyro.create_server()` -- `Pyro.list_environments()` -- `Pyro.pull_environment(environment)` -- `Pyro.inspect_environment(environment)` -- `Pyro.prune_environments()` -- `Pyro.create_vm(...)` -- `Pyro.create_workspace(..., name=None, labels=None, network_policy="off", secrets=None)` -- `Pyro.list_workspaces()` -- `Pyro.push_workspace_sync(workspace_id, source_path, *, dest="/workspace")` -- `Pyro.stop_workspace(workspace_id)` -- `Pyro.start_workspace(workspace_id)` -- `Pyro.list_workspace_files(workspace_id, path="/workspace", recursive=False)` -- `Pyro.read_workspace_file(workspace_id, path, *, max_bytes=65536)` -- `Pyro.write_workspace_file(workspace_id, path, *, text)` -- `Pyro.export_workspace(workspace_id, path, *, output_path)` -- `Pyro.apply_workspace_patch(workspace_id, *, patch)` -- `Pyro.export_workspace_disk(workspace_id, *, output_path)` -- `Pyro.list_workspace_disk(workspace_id, path="/workspace", recursive=False)` -- `Pyro.read_workspace_disk(workspace_id, path, *, max_bytes=65536)` -- `Pyro.diff_workspace(workspace_id)` -- `Pyro.create_snapshot(workspace_id, snapshot_name)` -- `Pyro.list_snapshots(workspace_id)` -- `Pyro.delete_snapshot(workspace_id, snapshot_name)` -- `Pyro.reset_workspace(workspace_id, *, snapshot="baseline")` -- `Pyro.start_service(workspace_id, service_name, *, command, cwd="/workspace", readiness=None, ready_timeout_seconds=30, ready_interval_ms=500, secret_env=None, published_ports=None)` -- `Pyro.list_services(workspace_id)` -- `Pyro.status_service(workspace_id, service_name)` -- `Pyro.logs_service(workspace_id, service_name, *, tail_lines=200, all=False)` -- `Pyro.stop_service(workspace_id, service_name)` -- `Pyro.open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30, secret_env=None)` -- `Pyro.read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536, plain=False, wait_for_idle_ms=None)` -- `Pyro.write_shell(workspace_id, shell_id, *, input, append_newline=True)` -- `Pyro.signal_shell(workspace_id, shell_id, *, signal_name="INT")` -- `Pyro.close_shell(workspace_id, shell_id)` -- `Pyro.start_vm(vm_id)` -- `Pyro.exec_vm(vm_id, *, command, timeout_seconds=30)` -- `Pyro.exec_workspace(workspace_id, *, command, timeout_seconds=30, secret_env=None)` -- `Pyro.stop_vm(vm_id)` -- `Pyro.delete_vm(vm_id)` -- `Pyro.delete_workspace(workspace_id)` -- `Pyro.status_vm(vm_id)` -- `Pyro.status_workspace(workspace_id)` -- `Pyro.update_workspace(workspace_id, *, name=None, clear_name=False, labels=None, clear_labels=None)` -- `Pyro.logs_workspace(workspace_id)` -- `Pyro.network_info_vm(vm_id)` -- `Pyro.reap_expired()` -- `Pyro.run_in_vm(...)` +Host-specific setup docs: -Stable public method names: +- [claude_code_mcp.md](../examples/claude_code_mcp.md) +- [codex_mcp.md](../examples/codex_mcp.md) +- [opencode_mcp_config.json](../examples/opencode_mcp_config.json) +- [mcp_client_config.md](../examples/mcp_client_config.md) -- `create_server()` -- `list_environments()` -- `pull_environment(environment)` -- `inspect_environment(environment)` -- `prune_environments()` -- `create_vm(...)` -- `create_workspace(..., name=None, labels=None, network_policy="off", secrets=None)` -- `list_workspaces()` -- `push_workspace_sync(workspace_id, source_path, *, dest="/workspace")` -- `stop_workspace(workspace_id)` -- `start_workspace(workspace_id)` -- `list_workspace_files(workspace_id, path="/workspace", recursive=False)` -- `read_workspace_file(workspace_id, path, *, max_bytes=65536)` -- `write_workspace_file(workspace_id, path, *, text)` -- `export_workspace(workspace_id, path, *, output_path)` -- `apply_workspace_patch(workspace_id, *, patch)` -- `export_workspace_disk(workspace_id, *, output_path)` -- `list_workspace_disk(workspace_id, path="/workspace", recursive=False)` -- `read_workspace_disk(workspace_id, path, *, max_bytes=65536)` -- `diff_workspace(workspace_id)` -- `create_snapshot(workspace_id, snapshot_name)` -- `list_snapshots(workspace_id)` -- `delete_snapshot(workspace_id, snapshot_name)` -- `reset_workspace(workspace_id, *, snapshot="baseline")` -- `start_service(workspace_id, service_name, *, command, cwd="/workspace", readiness=None, ready_timeout_seconds=30, ready_interval_ms=500, secret_env=None, published_ports=None)` -- `list_services(workspace_id)` -- `status_service(workspace_id, service_name)` -- `logs_service(workspace_id, service_name, *, tail_lines=200, all=False)` -- `stop_service(workspace_id, service_name)` -- `open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30, secret_env=None)` -- `read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536, plain=False, wait_for_idle_ms=None)` -- `write_shell(workspace_id, shell_id, *, input, append_newline=True)` -- `signal_shell(workspace_id, shell_id, *, signal_name="INT")` -- `close_shell(workspace_id, shell_id)` -- `start_vm(vm_id)` -- `exec_vm(vm_id, *, command, timeout_seconds=30)` -- `exec_workspace(workspace_id, *, command, timeout_seconds=30, secret_env=None)` -- `stop_vm(vm_id)` -- `delete_vm(vm_id)` -- `delete_workspace(workspace_id)` -- `status_vm(vm_id)` -- `status_workspace(workspace_id)` -- `update_workspace(workspace_id, *, name=None, clear_name=False, labels=None, clear_labels=None)` -- `logs_workspace(workspace_id)` -- `network_info_vm(vm_id)` -- `reap_expired()` -- `run_in_vm(...)` +## Chat-Facing Workspace Contract -Behavioral defaults: - -- `Pyro.create_vm(...)` and `Pyro.run_in_vm(...)` default to `vcpu_count=1` and `mem_mib=1024`. -- `Pyro.create_workspace(...)` defaults to `vcpu_count=1` and `mem_mib=1024`. -- `allow_host_compat` defaults to `False` on `create_vm(...)` and `run_in_vm(...)`. -- `allow_host_compat` defaults to `False` on `create_workspace(...)`. -- `Pyro.create_workspace(..., seed_path=...)` seeds `/workspace` from a host directory or a local `.tar` / `.tar.gz` / `.tgz` archive before the workspace is returned. -- `Pyro.create_workspace(..., name=..., labels=...)` attaches human-oriented discovery metadata without changing the stable `workspace_id`. -- `Pyro.create_workspace(..., network_policy="off"|"egress"|"egress+published-ports")` controls workspace guest networking and whether services may publish host ports. -- `Pyro.create_workspace(..., secrets=...)` persists guest-only UTF-8 secrets outside `/workspace`. -- `Pyro.list_workspaces()` returns persisted workspace summaries sorted by most recent `last_activity_at`. -- `Pyro.push_workspace_sync(...)` imports later host-side directory or archive content into a started workspace. -- `Pyro.stop_workspace(...)` stops one persistent workspace without deleting its `/workspace`, snapshots, or command history. -- `Pyro.start_workspace(...)` restarts one stopped workspace without resetting `/workspace`. -- `Pyro.list_workspace_files(...)`, `Pyro.read_workspace_file(...)`, and `Pyro.write_workspace_file(...)` provide structured live `/workspace` inspection and text edits without shell quoting. -- `Pyro.export_workspace(...)` exports one file or directory from `/workspace` to an explicit host path. -- `Pyro.apply_workspace_patch(...)` applies unified text patches for add/modify/delete operations under `/workspace`. -- `Pyro.export_workspace_disk(...)` copies the stopped guest-backed workspace rootfs as raw ext4 to an explicit host path. -- `Pyro.list_workspace_disk(...)` inspects a stopped guest-backed workspace rootfs offline without booting the guest. -- `Pyro.read_workspace_disk(...)` reads one regular file from a stopped guest-backed workspace rootfs offline. -- stopped-workspace disk helpers require `state=stopped` and a guest-backed workspace; they fail on `host_compat`. -- `Pyro.diff_workspace(...)` compares the current `/workspace` tree to the immutable create-time baseline. -- `Pyro.create_snapshot(...)` captures one named `/workspace` checkpoint. -- `Pyro.list_snapshots(...)` lists the implicit `baseline` plus any named snapshots. -- `Pyro.delete_snapshot(...)` deletes one named snapshot while leaving `baseline` intact. -- `Pyro.reset_workspace(...)` recreates the full sandbox from `baseline` or one named snapshot and clears command, shell, and service history. -- `Pyro.start_service(..., secret_env=...)` maps persisted workspace secrets into that service process as environment variables for that start call only. -- `Pyro.start_service(...)` starts one named long-running process in a started workspace and waits for its typed readiness probe when configured. -- `Pyro.start_service(..., published_ports=[...])` publishes one or more guest TCP ports to `127.0.0.1` on the host when the workspace network policy is `egress+published-ports`. -- `Pyro.list_services(...)`, `Pyro.status_service(...)`, `Pyro.logs_service(...)`, and `Pyro.stop_service(...)` manage those persisted workspace services. -- `Pyro.exec_vm(...)` runs one command and auto-cleans that VM after the exec completes. -- `Pyro.exec_workspace(..., secret_env=...)` maps persisted workspace secrets into that exec call as environment variables for that call only. -- `Pyro.exec_workspace(...)` runs one command in the persistent workspace and leaves it alive. -- `Pyro.open_shell(..., secret_env=...)` maps persisted workspace secrets into the shell environment when that shell opens. -- `Pyro.open_shell(...)` opens a persistent PTY shell attached to one started workspace. -- `Pyro.read_shell(...)` reads merged text output from that shell by cursor, with optional plain rendering and idle batching for chat-facing consumers. -- `Pyro.write_shell(...)`, `Pyro.signal_shell(...)`, and `Pyro.close_shell(...)` operate on that persistent shell session. -- `Pyro.update_workspace(...)` changes only discovery metadata such as `name` and key/value `labels`. - -## MCP Contract - -Stable MCP profiles: - -- `vm-run`: exposes only `vm_run` -- `workspace-core`: exposes `vm_run`, `workspace_create`, `workspace_list`, `workspace_update`, `workspace_status`, `workspace_sync_push`, `workspace_exec`, `workspace_logs`, `workspace_file_list`, `workspace_file_read`, `workspace_file_write`, `workspace_patch_apply`, `workspace_diff`, `workspace_export`, `workspace_reset`, and `workspace_delete` -- `workspace-full`: exposes the complete stable MCP surface below - -Behavioral defaults: - -- `pyro mcp serve`, `create_server()`, and `Pyro.create_server()` default to `workspace-core`. -- `workspace-core` is the default and recommended first profile for most new chat-host integrations. -- `create_server(profile="workspace-full")` and `Pyro.create_server(profile="workspace-full")` opt into the full advanced workspace surface explicitly. -- `workspace-core` narrows `workspace_create` by omitting `network_policy` and `secrets`. -- `workspace-core` narrows `workspace_exec` by omitting `secret_env`. - -Primary tool: +`workspace-core` is the normal chat path. It exposes: - `vm_run` - -Advanced lifecycle tools: - -- `vm_list_environments` -- `vm_create` -- `vm_start` -- `vm_exec` -- `vm_stop` -- `vm_delete` -- `vm_status` -- `vm_network_info` -- `vm_reap_expired` - -Persistent workspace tools: - - `workspace_create` - `workspace_list` +- `workspace_update` +- `workspace_status` - `workspace_sync_push` -- `workspace_stop` -- `workspace_start` - `workspace_exec` +- `workspace_logs` - `workspace_file_list` - `workspace_file_read` - `workspace_file_write` -- `workspace_export` - `workspace_patch_apply` -- `workspace_disk_export` -- `workspace_disk_list` -- `workspace_disk_read` - `workspace_diff` -- `snapshot_create` -- `snapshot_list` -- `snapshot_delete` +- `workspace_export` - `workspace_reset` -- `service_start` -- `service_list` -- `service_status` -- `service_logs` -- `service_stop` -- `shell_open` -- `shell_read` -- `shell_write` -- `shell_signal` -- `shell_close` -- `workspace_status` -- `workspace_update` -- `workspace_logs` - `workspace_delete` -Behavioral defaults: +That is enough for the normal persistent editing loop: -- `vm_run` and `vm_create` default to `vcpu_count=1` and `mem_mib=1024`. -- `workspace_create` defaults to `vcpu_count=1` and `mem_mib=1024`. -- `vm_run` and `vm_create` expose `allow_host_compat`, which defaults to `false`. -- `workspace_create` exposes `allow_host_compat`, which defaults to `false`. -- `workspace_create` accepts optional `seed_path` and seeds `/workspace` from a host directory or a local `.tar` / `.tar.gz` / `.tgz` archive before the workspace is returned. -- `workspace_create` accepts optional `name` and `labels` metadata for human discovery without changing the stable `workspace_id`. -- `workspace_create` accepts `network_policy` with `off`, `egress`, or `egress+published-ports` to control workspace guest networking and service port publication. -- `workspace_create` accepts optional `secrets` and persists guest-only UTF-8 secret material outside `/workspace`. -- `workspace_list` returns persisted workspace summaries sorted by most recent `last_activity_at`. -- `workspace_sync_push` imports later host-side directory or archive content into a started workspace, with an optional `dest` under `/workspace`. -- `workspace_stop` stops one persistent workspace without deleting its `/workspace`, snapshots, or command history. -- `workspace_start` restarts one stopped workspace without resetting `/workspace`. -- `workspace_file_list`, `workspace_file_read`, and `workspace_file_write` provide structured live `/workspace` inspection and text edits without shell quoting. -- `workspace_export` exports one file or directory from `/workspace` to an explicit host path. -- `workspace_patch_apply` applies unified text patches for add/modify/delete operations under `/workspace`. -- `workspace_disk_export` copies the stopped guest-backed workspace rootfs as raw ext4 to an explicit host path. -- `workspace_disk_list` inspects a stopped guest-backed workspace rootfs offline without booting the guest. -- `workspace_disk_read` reads one regular file from a stopped guest-backed workspace rootfs offline. -- stopped-workspace disk tools require `state=stopped` and a guest-backed workspace; they fail on `host_compat`. -- `workspace_diff` compares the current `/workspace` tree to the immutable create-time baseline. -- `snapshot_create`, `snapshot_list`, and `snapshot_delete` manage explicit named snapshots in addition to the implicit `baseline`. -- `workspace_reset` recreates the full sandbox and restores `/workspace` from `baseline` or one named snapshot. -- `service_start`, `service_list`, `service_status`, `service_logs`, and `service_stop` manage persistent named services inside a started workspace. -- `service_start` accepts optional `published_ports` to expose guest TCP ports on `127.0.0.1` when the workspace network policy is `egress+published-ports`. -- `vm_exec` runs one command and auto-cleans that VM after the exec completes. -- `workspace_exec` accepts optional `secret_env` mappings for one exec call and leaves the workspace alive. -- `workspace_update` changes only discovery metadata such as `name` and key/value `labels`. -- `service_start` accepts optional `secret_env` mappings for one service start call. -- `shell_open` accepts optional `secret_env` mappings for the opened shell session. -- `shell_open`, `shell_read`, `shell_write`, `shell_signal`, and `shell_close` manage persistent PTY shells inside a started workspace. +- create one workspace +- sync or seed repo content +- inspect and edit files without shell quoting +- run commands repeatedly in one sandbox +- diff and export results +- reset and retry +- delete the workspace when the task is done -## Versioning Rule +Move to `workspace-full` only when the chat truly needs: -- `pyro-mcp` uses SemVer. -- Environment names are stable identifiers in the shipped catalog. -- Changing a public command name, public flag, public method name, public MCP tool name, or required request field is a breaking change. +- persistent PTY shell sessions +- long-running services and readiness probes +- secrets +- guest networking and published ports +- stopped-workspace disk inspection + +## Recipe-Backed Workflows + +The documented product workflows are: + +| Workflow | Recommended profile | Doc | +| --- | --- | --- | +| Cold-start repo validation | `workspace-full` | [use-cases/cold-start-repo-validation.md](use-cases/cold-start-repo-validation.md) | +| Repro plus fix loop | `workspace-core` | [use-cases/repro-fix-loop.md](use-cases/repro-fix-loop.md) | +| Parallel isolated workspaces | `workspace-core` | [use-cases/parallel-workspaces.md](use-cases/parallel-workspaces.md) | +| Unsafe or untrusted code inspection | `workspace-core` | [use-cases/untrusted-inspection.md](use-cases/untrusted-inspection.md) | +| Review and evaluation workflows | `workspace-full` | [use-cases/review-eval-workflows.md](use-cases/review-eval-workflows.md) | + +Treat this smoke pack as the trustworthy guest-backed verification path for the +advertised product: + +```bash +make smoke-use-cases +``` + +The chat-host MCP path above is the thing the docs are intentionally shaping +around. diff --git a/docs/use-cases/README.md b/docs/use-cases/README.md index 15911d6..0f93188 100644 --- a/docs/use-cases/README.md +++ b/docs/use-cases/README.md @@ -1,6 +1,6 @@ # Workspace Use-Case Recipes -These recipes turn the stable workspace surface into five concrete agent flows. +These recipes turn the chat-host workspace path into five concrete agent flows. They are the canonical next step after the quickstart in [install.md](../install.md) or [first-run.md](../first-run.md). diff --git a/docs/use-cases/cold-start-repo-validation.md b/docs/use-cases/cold-start-repo-validation.md index f856906..3b303be 100644 --- a/docs/use-cases/cold-start-repo-validation.md +++ b/docs/use-cases/cold-start-repo-validation.md @@ -12,26 +12,14 @@ Use this flow when an agent needs to treat a fresh repo like a new user would: seed it into a workspace, run the validation script, keep one long-running process alive, probe it from another command, and export a validation report. -Canonical SDK flow: +Chat-host recipe: -```python -from pyro_mcp import Pyro - -pyro = Pyro() -created = pyro.create_workspace(environment="debian:12", seed_path="./repo") -workspace_id = str(created["workspace_id"]) - -pyro.exec_workspace(workspace_id, command="sh validate.sh") -pyro.start_service( - workspace_id, - "app", - command="sh serve.sh", - readiness={"type": "file", "path": ".app-ready"}, -) -pyro.exec_workspace(workspace_id, command="sh -lc 'test -f .app-ready && cat service-state.txt'") -pyro.export_workspace(workspace_id, "validation-report.txt", output_path="./validation-report.txt") -pyro.delete_workspace(workspace_id) -``` +1. Create one workspace from the repo seed. +2. Run the validation command inside that workspace. +3. Start the app as a long-running service with readiness configured. +4. Probe the ready service from another command in the same workspace. +5. Export the validation report back to the host. +6. Delete the workspace when the evaluation is done. This recipe is intentionally guest-local and deterministic. It proves startup, service readiness, validation, and host-out report capture without depending on diff --git a/docs/use-cases/parallel-workspaces.md b/docs/use-cases/parallel-workspaces.md index ddc29b7..ccabd7a 100644 --- a/docs/use-cases/parallel-workspaces.md +++ b/docs/use-cases/parallel-workspaces.md @@ -11,32 +11,14 @@ make smoke-parallel-workspaces Use this flow when the agent needs one isolated workspace per issue, branch, or review thread and must rediscover the right one later. -Canonical SDK flow: +Chat-host recipe: -```python -from pyro_mcp import Pyro - -pyro = Pyro() -alpha = pyro.create_workspace( - environment="debian:12", - seed_path="./repo", - name="parallel-alpha", - labels={"branch": "alpha", "issue": "123"}, -) -beta = pyro.create_workspace( - environment="debian:12", - seed_path="./repo", - name="parallel-beta", - labels={"branch": "beta", "issue": "456"}, -) - -pyro.write_workspace_file(alpha["workspace_id"], "branch.txt", text="alpha\n") -pyro.write_workspace_file(beta["workspace_id"], "branch.txt", text="beta\n") -pyro.update_workspace(alpha["workspace_id"], labels={"branch": "alpha", "owner": "alice"}) -pyro.list_workspaces() -pyro.delete_workspace(alpha["workspace_id"]) -pyro.delete_workspace(beta["workspace_id"]) -``` +1. Create one workspace per issue or branch with a human-friendly name and + labels. +2. Mutate each workspace independently. +3. Rediscover the right workspace later with `workspace_list`. +4. Update metadata when ownership or issue mapping changes. +5. Delete each workspace independently when its task is done. The important proof here is operational, not syntactic: names, labels, list ordering, and file contents stay isolated even when multiple workspaces are diff --git a/docs/use-cases/repro-fix-loop.md b/docs/use-cases/repro-fix-loop.md index f302974..3550143 100644 --- a/docs/use-cases/repro-fix-loop.md +++ b/docs/use-cases/repro-fix-loop.md @@ -12,31 +12,15 @@ Use this flow when the agent has to reproduce a bug, patch files without shell quoting tricks, rerun the failing command, diff the result, export the fix, and reset back to baseline. -Canonical SDK flow: +Chat-host recipe: -```python -from pyro_mcp import Pyro - -pyro = Pyro() -created = pyro.create_workspace(environment="debian:12", seed_path="./broken-repro") -workspace_id = str(created["workspace_id"]) - -pyro.exec_workspace(workspace_id, command="sh check.sh") -pyro.read_workspace_file(workspace_id, "message.txt") -pyro.apply_workspace_patch( - workspace_id, - patch="--- a/message.txt\n+++ b/message.txt\n@@ -1 +1 @@\n-broken\n+fixed\n", -) -pyro.exec_workspace(workspace_id, command="sh check.sh") -pyro.diff_workspace(workspace_id) -pyro.export_workspace(workspace_id, "message.txt", output_path="./message.txt") -pyro.reset_workspace(workspace_id) -pyro.delete_workspace(workspace_id) -``` - -Canonical MCP/chat example: - -- [examples/openai_responses_workspace_core.py](../../examples/openai_responses_workspace_core.py) +1. Create one workspace from the broken repro seed. +2. Run the failing command. +3. Inspect the broken file with structured file reads. +4. Apply the fix with `workspace_patch_apply`. +5. Rerun the failing command in the same workspace. +6. Diff and export the changed result. +7. Reset to baseline and delete the workspace. This is the main `workspace-core` story: model-native file ops, repeatable exec, structured diff, explicit export, and reset-over-repair. diff --git a/docs/use-cases/review-eval-workflows.md b/docs/use-cases/review-eval-workflows.md index eabe981..1d8d12e 100644 --- a/docs/use-cases/review-eval-workflows.md +++ b/docs/use-cases/review-eval-workflows.md @@ -11,30 +11,14 @@ make smoke-review-eval Use this flow when an agent needs to read a checklist interactively, run an evaluation script, checkpoint or reset its changes, and export the final report. -Canonical SDK flow: +Chat-host recipe: -```python -from pyro_mcp import Pyro - -pyro = Pyro() -created = pyro.create_workspace(environment="debian:12", seed_path="./review-fixture") -workspace_id = str(created["workspace_id"]) - -pyro.create_snapshot(workspace_id, "pre-review") -shell = pyro.open_shell(workspace_id) -pyro.write_shell(workspace_id, shell["shell_id"], input="cat CHECKLIST.md") -pyro.read_shell( - workspace_id, - shell["shell_id"], - plain=True, - wait_for_idle_ms=300, -) -pyro.close_shell(workspace_id, shell["shell_id"]) -pyro.exec_workspace(workspace_id, command="sh review.sh") -pyro.export_workspace(workspace_id, "review-report.txt", output_path="./review-report.txt") -pyro.reset_workspace(workspace_id, snapshot="pre-review") -pyro.delete_workspace(workspace_id) -``` +1. Create a named snapshot before the review starts. +2. Open a readable PTY shell and inspect the checklist interactively. +3. Run the review or evaluation script in the same workspace. +4. Export the final report. +5. Reset back to the snapshot if the review branch goes sideways. +6. Delete the workspace when the evaluation is done. This is the stable shell-facing story: readable PTY output for chat loops, checkpointed evaluation, explicit export, and reset when a review branch goes diff --git a/docs/use-cases/untrusted-inspection.md b/docs/use-cases/untrusted-inspection.md index a089faa..6a7b85b 100644 --- a/docs/use-cases/untrusted-inspection.md +++ b/docs/use-cases/untrusted-inspection.md @@ -11,24 +11,13 @@ make smoke-untrusted-inspection Use this flow when the agent needs to inspect suspicious code or an unfamiliar repo without granting more capabilities than necessary. -Canonical SDK flow: +Chat-host recipe: -```python -from pyro_mcp import Pyro - -pyro = Pyro() -created = pyro.create_workspace(environment="debian:12", seed_path="./suspicious-repo") -workspace_id = str(created["workspace_id"]) - -pyro.list_workspace_files(workspace_id, path="/workspace", recursive=True) -pyro.read_workspace_file(workspace_id, "suspicious.sh") -pyro.exec_workspace( - workspace_id, - command="sh -lc \"grep -n 'curl' suspicious.sh > inspection-report.txt\"", -) -pyro.export_workspace(workspace_id, "inspection-report.txt", output_path="./inspection-report.txt") -pyro.delete_workspace(workspace_id) -``` +1. Create one workspace from the suspicious repo seed. +2. Inspect the tree with structured file listing and file reads. +3. Run the smallest possible command that produces the inspection report. +4. Export only the report the agent chose to materialize. +5. Delete the workspace when inspection is complete. This recipe stays offline-by-default, uses only explicit file reads and execs, and exports only the inspection report the agent chose to materialize. diff --git a/docs/vision.md b/docs/vision.md index cdf852c..2192cce 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -1,16 +1,19 @@ # Vision -`pyro-mcp` should become the disposable sandbox where an agent can do real -development work safely, repeatedly, and reproducibly. +`pyro-mcp` should become the disposable MCP workspace for chat-based coding +agents. -That is a different product from a generic VM wrapper, a secure CI runner, or a -task queue with better isolation. +That is a different product from a generic VM wrapper, a secure CI runner, or +an SDK-first platform. + +`pyro-mcp` currently has no users. That means we can still make breaking +changes freely while we shape the chat-host path into the right product. ## Core Thesis The goal is not just to run one command in a microVM. -The goal is to give an LLM or coding agent a bounded workspace where it can: +The goal is to give a chat-hosted coding agent a bounded workspace where it can: - inspect a repo - install dependencies @@ -23,6 +26,25 @@ The goal is to give an LLM or coding agent a bounded workspace where it can: The sandbox is the execution boundary for agentic software work. +## Current Product Focus + +The product path should be obvious and narrow: + +- Claude Code +- Codex +- OpenCode +- Linux `x86_64` with KVM + +The happy path is: + +1. prove the host with the terminal companion commands +2. run `pyro mcp serve` +3. connect a chat host +4. work through one disposable workspace per task + +The repo can contain lower-level building blocks, but they should not drive the +product story. + ## What This Is Not `pyro-mcp` should not drift into: @@ -32,9 +54,10 @@ The sandbox is the execution boundary for agentic software work. - a generic CI job runner - a scheduler or queueing platform - a broad VM orchestration product +- an SDK product that happens to have an MCP server on the side -Those products optimize for queued work, throughput, retries, matrix builds, and -shared infrastructure. +Those products optimize for queued work, throughput, retries, matrix builds, or +library ergonomics. `pyro-mcp` should optimize for agent loops: @@ -57,10 +80,15 @@ Any sandbox product starts to look like CI if the main abstraction is: That shape is useful, but it is not the center of the vision. To stay aligned, the primary abstraction should be a workspace the agent -inhabits, not a job the agent submits. +inhabits from a chat host, not a job the agent submits to a runner. ## Product Principles +### Chat Hosts First + +The product should be shaped around the MCP path used from chat interfaces. +Everything else is there to support, debug, or build that path. + ### Workspace-First The default mental model should be "open a disposable workspace" rather than @@ -85,11 +113,6 @@ Anything that crosses the host boundary should be intentional and visible: Agents should be able to checkpoint, reset, and retry cheaply. Disposable state is a feature, not a limitation. -### Same Contract Across Surfaces - -CLI, Python, and MCP should expose the same underlying workspace model so the -product feels coherent no matter how it is consumed. - ### Agent-Native Observability The sandbox should expose the things an agent actually needs to reason about: @@ -101,10 +124,16 @@ The sandbox should expose the things an agent actually needs to reason about: - readiness - exported results -## The Shape Of An LLM-First Sandbox +## The Shape Of The Product -The strongest future direction is a small, agent-native contract built around -workspaces, shells, files, services, and reset. +The strongest direction is a small chat-facing contract built around: + +- one MCP server +- one disposable workspace model +- structured file inspection and edits +- repeated commands in the same sandbox +- service lifecycle when the workflow needs it +- reset as a first-class workflow primitive Representative primitives: @@ -114,95 +143,57 @@ Representative primitives: - `workspace.sync_push` - `workspace.export` - `workspace.diff` -- `workspace.snapshot` - `workspace.reset` +- `workspace.exec` - `shell.open` - `shell.read` - `shell.write` -- `shell.signal` -- `shell.close` -- `workspace.exec` - `service.start` - `service.status` - `service.logs` -- `service.stop` -These names are illustrative, not a committed public API. - -The important point is the interaction model: - -- a shell session is interactive state inside the sandbox -- a workspace is durable for the life of the task -- services are first-class, not accidental background jobs -- reset is a core workflow primitive +These names are illustrative, not a promise that every lower-level repo surface +should be treated as equally stable or equally important. ## Interactive Shells And Disk Operations Interactive shells are aligned with the vision because they make the agent feel present inside the sandbox rather than reduced to one-shot job submission. -That does not mean `pyro-mcp` should become a raw SSH replacement. The shell -should sit inside a higher-level workspace model with structured file, service, -diff, and reset operations around it. +They should remain subordinate to the workspace model, not replace it with a +raw SSH story. -Disk-level operations are also useful, but they should remain supporting tools. -They are good for: +Disk-level operations are useful for: - fast workspace seeding - snapshotting - offline inspection -- diffing - export/import without a full boot -They should not become the primary product identity. If the center of the -product becomes "operate on VM disks", it will read as image tooling rather -than an agent workspace. +They should remain supporting tools rather than the product identity. ## What To Build Next -Features should be prioritized in this order: +Features should keep reinforcing the chat-host path in this order: -1. Repeated commands in one persistent workspace -2. Interactive shell sessions with PTY semantics -3. Structured workspace sync and export -4. Service lifecycle and readiness checks -5. Snapshot and reset workflows -6. Explicit secrets and network policy -7. Secondary disk-level import/export and inspection tools +1. make the first chat-host setup painfully obvious +2. make the recipe-backed workflows feel trivial from chat +3. keep the smoke pack trustworthy enough to gate the advertised stories +4. keep the terminal companion path good enough to debug what the chat sees +5. let lower-level repo surfaces move freely when the chat-host product needs it The completed workspace GA roadmap lives in [roadmap/task-workspace-ga.md](roadmap/task-workspace-ga.md). -The next implementation milestones that make those workflows feel natural from -chat-driven LLM interfaces live in +The follow-on milestones that make the chat-host path clearer live in [roadmap/llm-chat-ergonomics.md](roadmap/llm-chat-ergonomics.md). -## Naming Guidance - -Prefer language that reinforces the workspace model: - -- `workspace` -- `sandbox` -- `shell` -- `service` -- `snapshot` -- `reset` - -Avoid centering language that makes the product feel like CI infrastructure: - -- `job` -- `runner` -- `pipeline` -- `worker` -- `queue` -- `build matrix` - ## Litmus Test When evaluating a new feature, ask: -"Does this help an agent inhabit a safe disposable workspace and do real -software work inside it?" +"Does this make Claude Code, Codex, or OpenCode feel more natural and powerful +when they work inside a disposable sandbox?" -If the better description is "it helps submit, schedule, and report jobs", the -feature is probably pushing the product in the wrong direction. +If the better description is "it helps build a broader VM toolkit or SDK", it +is probably pushing the product in the wrong direction. diff --git a/pyproject.toml b/pyproject.toml index 2f078d7..b541162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pyro-mcp" version = "4.0.0" -description = "Stable Firecracker workspaces, one-shot sandboxes, and MCP tools for coding agents." +description = "Disposable MCP workspaces for chat-based coding agents on Linux KVM." readme = "README.md" license = { file = "LICENSE" } authors = [ @@ -9,7 +9,7 @@ authors = [ ] requires-python = ">=3.12" classifiers = [ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", diff --git a/src/pyro_mcp/cli.py b/src/pyro_mcp/cli.py index 47df4ee..c688984 100644 --- a/src/pyro_mcp/cli.py +++ b/src/pyro_mcp/cli.py @@ -639,18 +639,23 @@ class _HelpFormatter( def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( - "Run stable one-shot and persistent workspace workflows on supported " - "Linux x86_64 KVM hosts." + "Validate the host and serve disposable MCP workspaces for chat-based " + "coding agents on supported Linux x86_64 KVM hosts." ), epilog=dedent( """ - Suggested first run: + Suggested zero-to-hero path: pyro doctor pyro env list pyro env pull debian:12 pyro run debian:12 -- git --version + pyro mcp serve - Continue into the stable workspace path after that: + Connect a chat host after that: + claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve + codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve + + If you want terminal-level visibility into the workspace model: pyro workspace create debian:12 --seed-path ./repo --id-only pyro workspace sync push WORKSPACE_ID ./changes pyro workspace exec WORKSPACE_ID -- cat note.txt @@ -661,8 +666,6 @@ def _build_parser() -> argparse.ArgumentParser: pyro workspace service start WORKSPACE_ID app --ready-file .ready -- \ sh -lc 'touch .ready && while true; do sleep 60; done' pyro workspace export WORKSPACE_ID note.txt --output ./note.txt - - Use `pyro mcp serve` only after the CLI validation path works. """ ), formatter_class=_HelpFormatter, @@ -760,8 +763,8 @@ def _build_parser() -> argparse.ArgumentParser: help="Run the MCP server.", description=( "Run the MCP server after you have already validated the host and " - "guest execution with `pyro doctor` and `pyro run`. Bare `pyro " - "mcp serve` now starts the recommended `workspace-core` profile." + "guest execution with `pyro doctor` and `pyro run`. This is the " + "main product path for Claude Code, Codex, and OpenCode." ), epilog=dedent( """ @@ -790,11 +793,11 @@ def _build_parser() -> argparse.ArgumentParser: Profiles: workspace-core: default for normal persistent chat editing vm-run: smallest one-shot-only surface - workspace-full: advanced 4.x opt-in surface for shells, services, + workspace-full: larger opt-in surface for shells, services, snapshots, secrets, network policy, and disk tools - Use --profile workspace-full only when the host truly needs the full - advanced workspace surface. + Use --profile workspace-full only when the host truly needs those + extra workspace capabilities. """ ), formatter_class=_HelpFormatter, @@ -806,7 +809,7 @@ def _build_parser() -> argparse.ArgumentParser: help=( "Expose only one model-facing tool profile. `workspace-core` is " "the default and recommended first profile for most chat hosts; " - "`workspace-full` is the explicit advanced opt-in surface." + "`workspace-full` is the larger opt-in profile." ), ) @@ -888,7 +891,7 @@ def _build_parser() -> argparse.ArgumentParser: "workspace", help="Manage persistent workspaces.", description=( - "Use the stable workspace contract when you need one sandbox to stay alive " + "Use the workspace model when you need one sandbox to stay alive " "across repeated exec, shell, service, diff, export, snapshot, and reset calls." ), epilog=dedent( @@ -1818,7 +1821,7 @@ def _build_parser() -> argparse.ArgumentParser: pyro workspace service stop WORKSPACE_ID app Use `--ready-file` by default in the curated Debian environments. `--ready-command` - remains available as an escape hatch. + remains available when the workflow needs a custom readiness check. """ ), formatter_class=_HelpFormatter, diff --git a/tests/test_cli.py b/tests/test_cli.py index aaef8d1..7b276ab 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,17 +26,20 @@ def test_cli_help_guides_first_run() -> None: parser = cli._build_parser() help_text = parser.format_help() - assert "Suggested first run:" in help_text + assert "Suggested zero-to-hero path:" in help_text assert "pyro doctor" in help_text assert "pyro env list" in help_text assert "pyro env pull debian:12" in help_text assert "pyro run debian:12 -- git --version" in help_text - assert "Continue into the stable workspace path after that:" in help_text + assert "pyro mcp serve" in help_text + assert "Connect a chat host after that:" in help_text + assert "claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve" in help_text + assert "codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve" in help_text + assert "If you want terminal-level visibility into the workspace model:" in help_text assert "pyro workspace exec WORKSPACE_ID -- cat note.txt" in help_text assert "pyro workspace snapshot create WORKSPACE_ID checkpoint" in help_text assert "pyro workspace reset WORKSPACE_ID --snapshot checkpoint" in help_text assert "pyro workspace sync push WORKSPACE_ID ./changes" in help_text - assert "Use `pyro mcp serve` only after the CLI validation path works." in help_text def test_cli_subcommand_help_includes_examples_and_guidance() -> None: @@ -69,10 +72,10 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None: assert "vm-run" in mcp_help assert "recommended first profile for most chat hosts" in mcp_help assert "workspace-core: default for normal persistent chat editing" in mcp_help - assert "workspace-full: advanced 4.x opt-in surface" in mcp_help + assert "workspace-full: larger opt-in surface" in mcp_help workspace_help = _subparser_choice(parser, "workspace").format_help() - assert "stable workspace contract" in workspace_help + assert "Use the workspace model when you need one sandbox to stay alive" in workspace_help assert "pyro workspace create debian:12 --seed-path ./repo" in workspace_help assert "--id-only" in workspace_help assert "pyro workspace create debian:12 --name repro-fix --label issue=123" in workspace_help @@ -2822,9 +2825,9 @@ def test_chat_host_docs_and_examples_recommend_workspace_core() -> None: assert claude_cmd in readme assert codex_cmd in readme assert "examples/opencode_mcp_config.json" in readme - assert "recommended first profile for normal persistent chat editing" in readme + assert "Bare `pyro mcp serve` starts `workspace-core`" in readme - assert "## Chat Host Quickstart" in install + assert "## 5. Connect a chat host" in install assert "uvx --from pyro-mcp pyro mcp serve" in install assert claude_cmd in install assert codex_cmd in install @@ -2833,14 +2836,14 @@ def test_chat_host_docs_and_examples_recommend_workspace_core() -> None: assert claude_cmd in first_run assert codex_cmd in first_run - assert "Bare `pyro mcp serve` now starts `workspace-core`." in integrations + assert ( + "Bare `pyro mcp serve` starts `workspace-core`. That is the product path." + in integrations + ) assert "examples/claude_code_mcp.md" in integrations assert "examples/codex_mcp.md" in integrations assert "examples/opencode_mcp_config.json" in integrations - assert ( - '`Pyro.create_server()` for most chat hosts now that `workspace-core` ' - "is the default profile" in integrations - ) + assert "That is the product path." in integrations assert "Default for most chat hosts in `4.x`: `workspace-core`." in mcp_config assert "Use the host-specific examples first when they apply:" in mcp_config @@ -2882,7 +2885,7 @@ def test_content_only_read_docs_are_aligned() -> None: assert 'workspace file read "$WORKSPACE_ID" note.txt --content-only' in readme assert 'workspace file read "$WORKSPACE_ID" note.txt --content-only' in install assert 'workspace file read "$WORKSPACE_ID" note.txt --content-only' in first_run - assert 'workspace disk read "$WORKSPACE_ID" note.txt --content-only' in first_run + assert 'workspace patch apply "$WORKSPACE_ID" --patch-file fix.patch' in first_run def test_cli_workspace_shell_write_signal_close_json(