Add workspace snapshots and full reset

Implement the 2.8.0 workspace milestone with named snapshots and full-sandbox reset across the CLI, Python SDK, and MCP server.

Persist the immutable baseline plus named snapshot archives under each workspace, add workspace reset metadata, and make reset recreate the sandbox while clearing command history, shells, and services without changing the workspace identity or diff baseline.

Refresh the 2.8.0 docs, roadmap, and Python example around reset-over-repair, then validate with uv lock, UV_CACHE_DIR=.uv-cache make check, UV_CACHE_DIR=.uv-cache make dist-check, and a real guest-backed create/snapshot/reset/diff smoke test outside the sandbox.
This commit is contained in:
Thales Maciel 2026-03-12 12:41:11 -03:00
parent f504f0a331
commit 18b8fd2a7d
20 changed files with 1429 additions and 29 deletions

View file

@ -46,6 +46,10 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
assert "service_status" in tool_names
assert "service_logs" in tool_names
assert "service_stop" in tool_names
assert "snapshot_create" in tool_names
assert "snapshot_delete" in tool_names
assert "snapshot_list" in tool_names
assert "workspace_reset" in tool_names
def test_vm_run_round_trip(tmp_path: Path) -> None:
@ -234,6 +238,15 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
diffed = _extract_structured(
await server.call_tool("workspace_diff", {"workspace_id": workspace_id})
)
snapshot = _extract_structured(
await server.call_tool(
"snapshot_create",
{"workspace_id": workspace_id, "snapshot_name": "checkpoint"},
)
)
snapshots = _extract_structured(
await server.call_tool("snapshot_list", {"workspace_id": workspace_id})
)
export_path = tmp_path / "exported-more.txt"
exported = _extract_structured(
await server.call_tool(
@ -287,6 +300,18 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
},
)
)
reset = _extract_structured(
await server.call_tool(
"workspace_reset",
{"workspace_id": workspace_id, "snapshot": "checkpoint"},
)
)
deleted_snapshot = _extract_structured(
await server.call_tool(
"snapshot_delete",
{"workspace_id": workspace_id, "snapshot_name": "checkpoint"},
)
)
logs = _extract_structured(
await server.call_tool("workspace_logs", {"workspace_id": workspace_id})
)
@ -298,12 +323,16 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
synced,
executed,
diffed,
snapshot,
snapshots,
exported,
service,
services,
service_status,
service_logs,
service_stopped,
reset,
deleted_snapshot,
logs,
deleted,
)
@ -313,12 +342,16 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
synced,
executed,
diffed,
snapshot,
snapshots,
exported,
service,
services,
service_status,
service_logs,
service_stopped,
reset,
deleted_snapshot,
logs,
deleted,
) = asyncio.run(_run())
@ -327,6 +360,11 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
assert synced["workspace_sync"]["destination"] == "/workspace/subdir"
assert executed["stdout"] == "more\n"
assert diffed["changed"] is True
assert snapshot["snapshot"]["snapshot_name"] == "checkpoint"
assert [entry["snapshot_name"] for entry in snapshots["snapshots"]] == [
"baseline",
"checkpoint",
]
assert exported["artifact_type"] == "file"
assert Path(str(exported["output_path"])).read_text(encoding="utf-8") == "more\n"
assert service["state"] == "running"
@ -334,5 +372,9 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
assert service_status["state"] == "running"
assert service_logs["tail_lines"] is None
assert service_stopped["state"] == "stopped"
assert logs["count"] == 1
assert reset["workspace_reset"]["snapshot_name"] == "checkpoint"
assert reset["command_count"] == 0
assert reset["service_count"] == 0
assert deleted_snapshot["deleted"] is True
assert logs["count"] == 0
assert deleted["deleted"] is True