Add workspace export and baseline diff

Complete the 2.6.0 workspace milestone by adding explicit host-out export and immutable-baseline diff across the CLI, Python SDK, and MCP server.

Capture a baseline archive at workspace creation, export live /workspace paths through the guest agent, and compute structured whole-workspace diffs on the host without affecting command logs or shell state. The docs, roadmap, bundled guest agent, and workspace example now reflect the new create -> sync -> diff -> export workflow.

Validation: uv lock, UV_CACHE_DIR=.uv-cache make check, UV_CACHE_DIR=.uv-cache make dist-check, and a real guest-backed Firecracker smoke covering workspace create, sync push, diff, export, and delete.
This commit is contained in:
Thales Maciel 2026-03-12 03:15:45 -03:00
parent 3f8293ad24
commit 84a7e18d4d
26 changed files with 1492 additions and 43 deletions

View file

@ -22,7 +22,7 @@ Networking: tun=yes ip_forward=yes
```bash
$ uvx --from pyro-mcp pyro env list
Catalog version: 2.5.0
Catalog version: 2.6.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.
@ -72,6 +72,8 @@ deterministic structured result.
$ uvx --from pyro-mcp pyro demo
$ uvx --from pyro-mcp pyro workspace create debian:12 --seed-path ./repo
$ uvx --from pyro-mcp pyro workspace sync push WORKSPACE_ID ./changes
$ uvx --from pyro-mcp pyro workspace diff WORKSPACE_ID
$ uvx --from pyro-mcp pyro workspace export WORKSPACE_ID note.txt --output ./note.txt
$ uvx --from pyro-mcp pyro workspace shell open WORKSPACE_ID
$ uvx --from pyro-mcp pyro mcp serve
```
@ -98,6 +100,15 @@ $ 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 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 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
[workspace-shell-open] workspace_id=... shell_id=... state=running cwd=/workspace cols=120 rows=30 execution_mode=guest_vsock
@ -112,8 +123,10 @@ $ uvx --from pyro-mcp pyro workspace shell read WORKSPACE_ID SHELL_ID
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 `2.5.0`; if it fails partway through, delete and recreate the
workspace. Use `pyro workspace exec` for one-shot commands and `pyro workspace shell *` when you
workspace. Sync is non-atomic in `2.6.0`; if it fails partway through, delete and recreate the
workspace. Use `pyro workspace diff` to compare the current `/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 exec` for one-shot commands and `pyro workspace shell *` when you
need a persistent interactive PTY session in that same workspace.
Example output:

View file

@ -83,7 +83,7 @@ uvx --from pyro-mcp pyro env list
Expected output:
```bash
Catalog version: 2.5.0
Catalog version: 2.6.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.
@ -176,6 +176,8 @@ After the CLI path works, you can move on to:
- persistent workspaces: `pyro workspace create debian:12 --seed-path ./repo`
- live workspace updates: `pyro workspace sync push WORKSPACE_ID ./changes`
- baseline diff: `pyro workspace diff WORKSPACE_ID`
- host export: `pyro workspace export WORKSPACE_ID note.txt --output ./note.txt`
- interactive shells: `pyro workspace shell open WORKSPACE_ID`
- MCP: `pyro mcp serve`
- Python SDK: `from pyro_mcp import Pyro`
@ -189,6 +191,8 @@ Use `pyro workspace ...` when you need repeated commands in one sandbox instead
pyro workspace create debian:12 --seed-path ./repo
pyro workspace sync push WORKSPACE_ID ./changes --dest src
pyro workspace exec WORKSPACE_ID -- cat src/note.txt
pyro workspace diff WORKSPACE_ID
pyro workspace export WORKSPACE_ID src/note.txt --output ./note.txt
pyro workspace shell open WORKSPACE_ID
pyro workspace shell write WORKSPACE_ID SHELL_ID --input 'pwd'
pyro workspace shell read WORKSPACE_ID SHELL_ID
@ -201,9 +205,11 @@ Workspace commands default to the persistent `/workspace` directory inside the g
the identifier programmatically, use `--json` and read the `workspace_id` field. 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 `2.5.0`; if it fails partway through, delete and recreate the workspace from its
seed. Use `pyro workspace exec` for one-shot commands and `pyro workspace shell *` when you need
an interactive PTY that survives across separate calls.
is non-atomic in `2.6.0`; if it fails partway through, delete and recreate the workspace from its
seed. Use `pyro workspace diff` to compare the current 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 exec` for one-shot commands and `pyro workspace shell *` when you need an
interactive PTY that survives across separate calls.
## Contributor Clone

View file

@ -31,6 +31,7 @@ Recommended surface:
- `vm_run`
- `workspace_create(seed_path=...)` + `workspace_sync_push` + `workspace_exec` when the agent needs persistent workspace state
- `workspace_diff` + `workspace_export` when the agent needs explicit baseline comparison or host-out file transfer
- `open_shell` / `read_shell` / `write_shell` when the agent needs an interactive PTY inside that workspace
Canonical example:
@ -67,6 +68,7 @@ Recommended default:
- `Pyro.run_in_vm(...)`
- `Pyro.create_workspace(seed_path=...)` + `Pyro.push_workspace_sync(...)` + `Pyro.exec_workspace(...)` when repeated workspace commands are required
- `Pyro.diff_workspace(...)` + `Pyro.export_workspace(...)` when the agent needs baseline comparison or host-out file transfer
- `Pyro.open_shell(...)` + `Pyro.write_shell(...)` + `Pyro.read_shell(...)` when the agent needs an interactive PTY inside the workspace
Lifecycle note:
@ -78,6 +80,9 @@ Lifecycle note:
`/workspace` that starts from host content
- use `push_workspace_sync(...)` when later host-side changes need to be imported into that
running workspace without recreating it
- 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 `open_shell(...)` when the agent needs interactive shell state instead of one-shot execs
Examples:

View file

@ -22,6 +22,8 @@ Top-level commands:
- `pyro workspace create`
- `pyro workspace sync push`
- `pyro workspace exec`
- `pyro workspace export`
- `pyro workspace diff`
- `pyro workspace shell open`
- `pyro workspace shell read`
- `pyro workspace shell write`
@ -54,6 +56,8 @@ Behavioral guarantees:
- `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 sync push WORKSPACE_ID SOURCE_PATH [--dest WORKSPACE_PATH]` imports later host-side directory or archive content into a started workspace.
- `pyro workspace export WORKSPACE_ID PATH --output HOST_PATH` exports one file or directory from `/workspace` back to the host.
- `pyro workspace diff WORKSPACE_ID` compares the current `/workspace` tree to the immutable create-time baseline.
- `pyro workspace exec` runs in the persistent `/workspace` for that workspace and does not auto-clean.
- `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`.
@ -76,6 +80,8 @@ Supported public entrypoints:
- `Pyro.create_vm(...)`
- `Pyro.create_workspace(...)`
- `Pyro.push_workspace_sync(workspace_id, source_path, *, dest="/workspace")`
- `Pyro.export_workspace(workspace_id, path, *, output_path)`
- `Pyro.diff_workspace(workspace_id)`
- `Pyro.open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30)`
- `Pyro.read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536)`
- `Pyro.write_shell(workspace_id, shell_id, *, input, append_newline=True)`
@ -104,6 +110,8 @@ Stable public method names:
- `create_vm(...)`
- `create_workspace(...)`
- `push_workspace_sync(workspace_id, source_path, *, dest="/workspace")`
- `export_workspace(workspace_id, path, *, output_path)`
- `diff_workspace(workspace_id)`
- `open_shell(workspace_id, *, cwd="/workspace", cols=120, rows=30)`
- `read_shell(workspace_id, shell_id, *, cursor=0, max_chars=65536)`
- `write_shell(workspace_id, shell_id, *, input, append_newline=True)`
@ -130,6 +138,8 @@ Behavioral defaults:
- `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.push_workspace_sync(...)` imports later host-side directory or archive content into a started workspace.
- `Pyro.export_workspace(...)` exports one file or directory from `/workspace` to an explicit host path.
- `Pyro.diff_workspace(...)` compares the current `/workspace` tree to the immutable create-time baseline.
- `Pyro.exec_vm(...)` runs one command and auto-cleans that VM after the exec completes.
- `Pyro.exec_workspace(...)` runs one command in the persistent workspace and leaves it alive.
- `Pyro.open_shell(...)` opens a persistent PTY shell attached to one started workspace.
@ -159,6 +169,8 @@ Persistent workspace tools:
- `workspace_create`
- `workspace_sync_push`
- `workspace_exec`
- `workspace_export`
- `workspace_diff`
- `shell_open`
- `shell_read`
- `shell_write`
@ -176,6 +188,8 @@ Behavioral defaults:
- `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_sync_push` imports later host-side directory or archive content into a started workspace, with an optional `dest` under `/workspace`.
- `workspace_export` exports one file or directory from `/workspace` to an explicit host path.
- `workspace_diff` compares the current `/workspace` tree to the immutable create-time baseline.
- `vm_exec` runs one command and auto-cleans that VM after the exec completes.
- `workspace_exec` runs one command in a persistent `/workspace` and leaves the workspace alive.
- `shell_open`, `shell_read`, `shell_write`, `shell_signal`, and `shell_close` manage persistent PTY shells inside a started workspace.

View file

@ -2,12 +2,13 @@
This roadmap turns the agent-workspace vision into release-sized milestones.
Current baseline is `2.5.0`:
Current baseline is `2.6.0`:
- workspace persistence exists and the public surface is now workspace-first
- host crossing currently covers create-time seeding and later sync push
- host crossing currently covers create-time seeding, later sync push, and explicit export
- persistent PTY shell sessions exist alongside one-shot `workspace exec`
- no export, diff, service, snapshot, reset, or secrets contract exists yet
- immutable create-time baselines now power whole-workspace diff
- no service, snapshot, reset, or secrets contract exists yet
Locked roadmap decisions:
@ -28,7 +29,7 @@ also expected to update:
1. [`2.4.0` Workspace Contract Pivot](task-workspace-ga/2.4.0-workspace-contract-pivot.md) - Done
2. [`2.5.0` PTY Shell Sessions](task-workspace-ga/2.5.0-pty-shell-sessions.md) - Done
3. [`2.6.0` Structured Export And Baseline Diff](task-workspace-ga/2.6.0-structured-export-and-baseline-diff.md)
3. [`2.6.0` Structured Export And Baseline Diff](task-workspace-ga/2.6.0-structured-export-and-baseline-diff.md) - Done
4. [`2.7.0` Service Lifecycle And Typed Readiness](task-workspace-ga/2.7.0-service-lifecycle-and-typed-readiness.md)
5. [`2.8.0` Named Snapshots And Reset](task-workspace-ga/2.8.0-named-snapshots-and-reset.md)
6. [`2.9.0` Secrets](task-workspace-ga/2.9.0-secrets.md)

View file

@ -1,5 +1,7 @@
# `2.6.0` Structured Export And Baseline Diff
Status: Done
## Goal
Complete the next explicit host-crossing step by letting a workspace export