Add seeded task workspace creation

Current persistent tasks started with an empty workspace, which blocked the first useful host-to-task workflow in the task roadmap. This change lets task creation start from a host directory or tar archive without changing the one-shot VM surfaces.

Expose source_path on task create across the CLI, SDK, and MCP, add safe archive upload and extraction support for guest and host-compat backends, persist workspace_seed metadata, and patch the per-task rootfs with the bundled guest agent before boot so seeded guest tasks work without republishing environments. Also switch post--- command reconstruction to shlex.join() so documented sh -lc task examples preserve argument boundaries.

Validation:
- uv lock
- UV_CACHE_DIR=.uv-cache uv run pytest --no-cov tests/test_vm_guest.py tests/test_vm_manager.py tests/test_cli.py tests/test_api.py tests/test_server.py tests/test_public_contract.py
- UV_CACHE_DIR=.uv-cache make check
- UV_CACHE_DIR=.uv-cache make dist-check
- real guest-backed smoke: task create --source-path, task exec -- cat note.txt, task delete
This commit is contained in:
Thales Maciel 2026-03-11 21:45:38 -03:00
parent 58df176148
commit aa886b346e
25 changed files with 1076 additions and 75 deletions

View file

@ -18,7 +18,7 @@ It exposes the same runtime in three public forms:
- First run transcript: [docs/first-run.md](docs/first-run.md)
- 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 2.1.0: [CHANGELOG.md#210](CHANGELOG.md#210)
- What's new in 2.2.0: [CHANGELOG.md#220](CHANGELOG.md#220)
- 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)
@ -55,7 +55,7 @@ What success looks like:
```bash
Platform: linux-x86_64
Runtime: PASS
Catalog version: 2.1.0
Catalog version: 2.2.0
...
[pull] phase=install environment=debian:12
[pull] phase=ready environment=debian:12
@ -74,7 +74,7 @@ access to `registry-1.docker.io`, and needs local cache space for the guest imag
After the quickstart works:
- prove the full one-shot lifecycle with `uvx --from pyro-mcp pyro demo`
- create a persistent workspace with `uvx --from pyro-mcp pyro task create debian:12`
- create a persistent workspace with `uvx --from pyro-mcp pyro task create debian:12 --source-path ./repo`
- move to Python or MCP via [docs/integrations.md](docs/integrations.md)
## Supported Hosts
@ -128,7 +128,7 @@ uvx --from pyro-mcp pyro env list
Expected output:
```bash
Catalog version: 2.1.0
Catalog version: 2.2.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.
@ -198,7 +198,7 @@ Use `pyro run` for one-shot commands. Use `pyro task ...` when you need repeated
workspace without recreating the sandbox every time.
```bash
pyro task create debian:12
pyro task create debian:12 --source-path ./repo
pyro task exec TASK_ID -- sh -lc 'printf "hello from task\n" > note.txt'
pyro task exec TASK_ID -- cat note.txt
pyro task logs TASK_ID
@ -206,7 +206,9 @@ pyro task delete TASK_ID
```
Task workspaces start in `/workspace` and keep command history until you delete them. For machine
consumption, add `--json` and read the returned `task_id`.
consumption, add `--json` and read the returned `task_id`. Use `--source-path` when you want the
task to start from a host directory or a local `.tar` / `.tar.gz` / `.tgz` archive instead of an
empty workspace.
## Public Interfaces
@ -348,7 +350,7 @@ For repeated commands in one workspace:
from pyro_mcp import Pyro
pyro = Pyro()
task = pyro.create_task(environment="debian:12")
task = pyro.create_task(environment="debian:12", source_path="./repo")
task_id = task["task_id"]
try:
pyro.exec_task(task_id, command="printf 'hello from task\\n' > note.txt")
@ -378,7 +380,7 @@ Advanced lifecycle tools:
Persistent workspace tools:
- `task_create(environment, vcpu_count=1, mem_mib=1024, ttl_seconds=600, network=false, allow_host_compat=false)`
- `task_create(environment, vcpu_count=1, mem_mib=1024, ttl_seconds=600, network=false, allow_host_compat=false, source_path=null)`
- `task_exec(task_id, command, timeout_seconds=30)`
- `task_status(task_id)`
- `task_logs(task_id)`