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

@ -33,6 +33,8 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
assert "vm_run" in tool_names
assert "vm_status" in tool_names
assert "workspace_create" in tool_names
assert "workspace_diff" in tool_names
assert "workspace_export" in tool_names
assert "workspace_logs" in tool_names
assert "workspace_sync_push" in tool_names
assert "shell_open" in tool_names
@ -201,6 +203,8 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
dict[str, Any],
dict[str, Any],
dict[str, Any],
dict[str, Any],
dict[str, Any],
]:
server = create_server(manager=manager)
created = _extract_structured(
@ -236,6 +240,20 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
},
)
)
diffed = _extract_structured(
await server.call_tool("workspace_diff", {"workspace_id": workspace_id})
)
export_path = tmp_path / "exported-more.txt"
exported = _extract_structured(
await server.call_tool(
"workspace_export",
{
"workspace_id": workspace_id,
"path": "subdir/more.txt",
"output_path": str(export_path),
},
)
)
opened = _extract_structured(
await server.call_tool("shell_open", {"workspace_id": workspace_id})
)
@ -296,12 +314,27 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
deleted = _extract_structured(
await server.call_tool("workspace_delete", {"workspace_id": workspace_id})
)
return created, synced, executed, opened, written, read, signaled, closed, logs, deleted
return (
created,
synced,
executed,
diffed,
exported,
opened,
written,
read,
signaled,
closed,
logs,
deleted,
)
(
created,
synced,
executed,
diffed,
exported,
opened,
written,
read,
@ -314,6 +347,9 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
assert created["workspace_seed"]["mode"] == "directory"
assert synced["workspace_sync"]["destination"] == "/workspace/subdir"
assert executed["stdout"] == "more\n"
assert diffed["changed"] is True
assert exported["artifact_type"] == "file"
assert Path(str(exported["output_path"])).read_text(encoding="utf-8") == "more\n"
assert opened["state"] == "running"
assert written["input_length"] == 3
assert "/workspace" in read["output"]