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

@ -64,6 +64,8 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "pyro workspace create debian:12 --seed-path ./repo" in workspace_help
assert "pyro workspace sync push WORKSPACE_ID ./repo --dest src" in workspace_help
assert "pyro workspace exec WORKSPACE_ID" in workspace_help
assert "pyro workspace diff WORKSPACE_ID" in workspace_help
assert "pyro workspace export WORKSPACE_ID src/note.txt --output ./note.txt" in workspace_help
assert "pyro workspace shell open WORKSPACE_ID" in workspace_help
workspace_create_help = _subparser_choice(
@ -93,6 +95,18 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "--dest" in workspace_sync_push_help
assert "Import host content into `/workspace`" in workspace_sync_push_help
workspace_export_help = _subparser_choice(
_subparser_choice(parser, "workspace"), "export"
).format_help()
assert "--output" in workspace_export_help
assert "Export one file or directory from `/workspace`" in workspace_export_help
workspace_diff_help = _subparser_choice(
_subparser_choice(parser, "workspace"), "diff"
).format_help()
assert "immutable workspace baseline" in workspace_diff_help
assert "workspace export" in workspace_diff_help
workspace_shell_help = _subparser_choice(
_subparser_choice(parser, "workspace"),
"shell",
@ -522,6 +536,100 @@ def test_cli_workspace_exec_prints_human_output(
)
def test_cli_workspace_export_prints_human_output(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def export_workspace(
self,
workspace_id: str,
path: str,
*,
output_path: str,
) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert path == "note.txt"
assert output_path == "./note.txt"
return {
"workspace_id": workspace_id,
"workspace_path": "/workspace/note.txt",
"output_path": "/tmp/note.txt",
"artifact_type": "file",
"entry_count": 1,
"bytes_written": 6,
"execution_mode": "guest_vsock",
}
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="export",
workspace_id="workspace-123",
path="note.txt",
output="./note.txt",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
cli.main()
output = capsys.readouterr().out
assert "[workspace-export] workspace_id=workspace-123" in output
assert "artifact_type=file" in output
def test_cli_workspace_diff_prints_human_output(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def diff_workspace(self, workspace_id: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
return {
"workspace_id": workspace_id,
"changed": True,
"summary": {
"total": 1,
"added": 0,
"modified": 1,
"deleted": 0,
"type_changed": 0,
"text_patched": 1,
"non_text": 0,
},
"entries": [
{
"path": "note.txt",
"status": "modified",
"artifact_type": "file",
"text_patch": "--- a/note.txt\n+++ b/note.txt\n",
}
],
"patch": "--- a/note.txt\n+++ b/note.txt\n",
}
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="diff",
workspace_id="workspace-123",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
cli.main()
output = capsys.readouterr().out
assert (
"[workspace-diff] workspace_id=workspace-123 total=1 added=0 modified=1"
in output
)
assert "--- a/note.txt" in output
def test_cli_workspace_sync_push_prints_json(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None: