Remove shell-escaped file mutation from the stable workspace flow by adding explicit file and patch tools across the CLI, SDK, and MCP surfaces. This adds workspace file list/read/write plus unified text patch application, backed by new guest and manager file primitives that stay scoped to started workspaces and /workspace only. Patch application is preflighted on the host, file writes stay text-only and bounded, and the existing diff/export/reset semantics remain intact. The milestone also updates the 3.2.0 roadmap, public contract, docs, examples, and versioning, and includes focused coverage for the new helper module and dispatch paths. Validation: - uv lock - UV_CACHE_DIR=.uv-cache make check - UV_CACHE_DIR=.uv-cache make dist-check - real guest-backed smoke for workspace file read, patch apply, exec, export, and delete
338 lines
18 KiB
Markdown
338 lines
18 KiB
Markdown
# Public Contract
|
|
|
|
This document defines the stable public interface for `pyro-mcp` `3.x`.
|
|
|
|
## 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`
|
|
|
|
Stable product framing:
|
|
|
|
- `pyro run` is the stable one-shot entrypoint.
|
|
- `pyro workspace ...` is the stable persistent workspace contract.
|
|
|
|
## CLI Contract
|
|
|
|
Top-level commands:
|
|
|
|
- `pyro env list`
|
|
- `pyro env pull`
|
|
- `pyro env inspect`
|
|
- `pyro env prune`
|
|
- `pyro mcp serve`
|
|
- `pyro run`
|
|
- `pyro workspace create`
|
|
- `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 logs`
|
|
- `pyro workspace delete`
|
|
- `pyro doctor`
|
|
- `pyro demo`
|
|
- `pyro demo ollama`
|
|
|
|
Stable `pyro run` interface:
|
|
|
|
- positional environment name
|
|
- `--vcpu-count`
|
|
- `--mem-mib`
|
|
- `--timeout-seconds`
|
|
- `--ttl-seconds`
|
|
- `--network`
|
|
- `--allow-host-compat`
|
|
- `--json`
|
|
|
|
Behavioral guarantees:
|
|
|
|
- `pyro run <environment> -- <command>` 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 --network-policy {off,egress,egress+published-ports}` controls workspace guest networking and whether services may publish localhost ports.
|
|
- `pyro workspace create --secret NAME=VALUE` and `--secret-file NAME=PATH` persist guest-only UTF-8 secrets outside `/workspace`.
|
|
- `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]` reads one regular text file under `/workspace`.
|
|
- `pyro workspace file write WORKSPACE_ID PATH --text TEXT` creates or replaces 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]` 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` applies one unified text patch with add/modify/delete operations under `/workspace`.
|
|
- `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 logs` returns persisted command history for that workspace until `pyro workspace delete`.
|
|
- Workspace create/status results expose `workspace_seed` metadata describing how `/workspace` was initialized.
|
|
- 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 service start`, `pyro workspace service list`, and `pyro workspace service status` expose published-port metadata when present.
|
|
|
|
## Python SDK Contract
|
|
|
|
Primary facade:
|
|
|
|
- `Pyro`
|
|
|
|
Supported public entrypoints:
|
|
|
|
- `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(..., network_policy="off", secrets=None)`
|
|
- `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)`
|
|
- `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.logs_workspace(workspace_id)`
|
|
- `Pyro.network_info_vm(vm_id)`
|
|
- `Pyro.reap_expired()`
|
|
- `Pyro.run_in_vm(...)`
|
|
|
|
Stable public method names:
|
|
|
|
- `create_server()`
|
|
- `list_environments()`
|
|
- `pull_environment(environment)`
|
|
- `inspect_environment(environment)`
|
|
- `prune_environments()`
|
|
- `create_vm(...)`
|
|
- `create_workspace(..., network_policy="off", secrets=None)`
|
|
- `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)`
|
|
- `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)`
|
|
- `logs_workspace(workspace_id)`
|
|
- `network_info_vm(vm_id)`
|
|
- `reap_expired()`
|
|
- `run_in_vm(...)`
|
|
|
|
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(..., 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.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.
|
|
- `Pyro.write_shell(...)`, `Pyro.signal_shell(...)`, and `Pyro.close_shell(...)` operate on that persistent shell session.
|
|
|
|
## MCP Contract
|
|
|
|
Primary tool:
|
|
|
|
- `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_sync_push`
|
|
- `workspace_stop`
|
|
- `workspace_start`
|
|
- `workspace_exec`
|
|
- `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_reset`
|
|
- `service_start`
|
|
- `service_list`
|
|
- `service_status`
|
|
- `service_logs`
|
|
- `service_stop`
|
|
- `shell_open`
|
|
- `shell_read`
|
|
- `shell_write`
|
|
- `shell_signal`
|
|
- `shell_close`
|
|
- `workspace_status`
|
|
- `workspace_logs`
|
|
- `workspace_delete`
|
|
|
|
Behavioral defaults:
|
|
|
|
- `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 `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_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.
|
|
- `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.
|
|
|
|
## Versioning Rule
|
|
|
|
- `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.
|