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

@ -66,6 +66,8 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
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 snapshot create WORKSPACE_ID checkpoint" in workspace_help
assert "pyro workspace reset WORKSPACE_ID --snapshot checkpoint" in workspace_help
assert "pyro workspace shell open WORKSPACE_ID" in workspace_help
workspace_create_help = _subparser_choice(
@ -107,6 +109,34 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "immutable workspace baseline" in workspace_diff_help
assert "workspace export" in workspace_diff_help
workspace_snapshot_help = _subparser_choice(
_subparser_choice(parser, "workspace"),
"snapshot",
).format_help()
assert "pyro workspace snapshot create WORKSPACE_ID checkpoint" in workspace_snapshot_help
assert "baseline" in workspace_snapshot_help
workspace_snapshot_create_help = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"), "create"
).format_help()
assert "Capture the current `/workspace` tree" in workspace_snapshot_create_help
workspace_snapshot_list_help = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"), "list"
).format_help()
assert "baseline snapshot plus any named snapshots" in workspace_snapshot_list_help
workspace_snapshot_delete_help = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"), "delete"
).format_help()
assert "leaving the implicit baseline intact" in workspace_snapshot_delete_help
workspace_reset_help = _subparser_choice(
_subparser_choice(parser, "workspace"), "reset"
).format_help()
assert "--snapshot" in workspace_reset_help
assert "reset over repair" in workspace_reset_help
workspace_shell_help = _subparser_choice(
_subparser_choice(parser, "workspace"),
"shell",
@ -651,6 +681,359 @@ def test_cli_workspace_diff_prints_human_output(
assert "--- a/note.txt" in output
def test_cli_workspace_snapshot_create_list_delete_and_reset_prints_json(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def create_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot_name == "checkpoint"
return {
"workspace_id": workspace_id,
"snapshot": {
"snapshot_name": snapshot_name,
"kind": "named",
"entry_count": 3,
"bytes_written": 42,
},
}
def list_snapshots(self, workspace_id: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
return {
"workspace_id": workspace_id,
"count": 2,
"snapshots": [
{
"snapshot_name": "baseline",
"kind": "baseline",
"entry_count": 1,
"bytes_written": 10,
"deletable": False,
},
{
"snapshot_name": "checkpoint",
"kind": "named",
"entry_count": 3,
"bytes_written": 42,
"deletable": True,
},
],
}
def reset_workspace(self, workspace_id: str, *, snapshot: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot == "checkpoint"
return {
"workspace_id": workspace_id,
"state": "started",
"workspace_path": "/workspace",
"reset_count": 2,
"workspace_reset": {
"snapshot_name": snapshot,
"kind": "named",
"destination": "/workspace",
"entry_count": 3,
"bytes_written": 42,
},
}
def delete_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot_name == "checkpoint"
return {
"workspace_id": workspace_id,
"snapshot_name": snapshot_name,
"deleted": True,
}
class SnapshotCreateParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="create",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=True,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotCreateParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
cli.main()
created = json.loads(capsys.readouterr().out)
assert created["snapshot"]["snapshot_name"] == "checkpoint"
class SnapshotListParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="list",
workspace_id="workspace-123",
json=True,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotListParser())
cli.main()
listed = json.loads(capsys.readouterr().out)
assert listed["count"] == 2
class ResetParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="reset",
workspace_id="workspace-123",
snapshot="checkpoint",
json=True,
)
monkeypatch.setattr(cli, "_build_parser", lambda: ResetParser())
cli.main()
reset = json.loads(capsys.readouterr().out)
assert reset["workspace_reset"]["snapshot_name"] == "checkpoint"
class SnapshotDeleteParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="delete",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=True,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotDeleteParser())
cli.main()
deleted = json.loads(capsys.readouterr().out)
assert deleted["deleted"] is True
def test_cli_workspace_reset_prints_human_output(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def reset_workspace(self, workspace_id: str, *, snapshot: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot == "baseline"
return {
"workspace_id": workspace_id,
"state": "started",
"environment": "debian:12",
"workspace_path": "/workspace",
"workspace_seed": {
"mode": "directory",
"seed_path": "/tmp/repo",
"destination": "/workspace",
"entry_count": 1,
"bytes_written": 4,
},
"execution_mode": "guest_vsock",
"command_count": 0,
"service_count": 0,
"running_service_count": 0,
"reset_count": 3,
"last_reset_at": 123.0,
"workspace_reset": {
"snapshot_name": "baseline",
"kind": "baseline",
"destination": "/workspace",
"entry_count": 1,
"bytes_written": 4,
},
}
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="reset",
workspace_id="workspace-123",
snapshot="baseline",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
cli.main()
output = capsys.readouterr().out
assert "Reset source: baseline (baseline)" in output
assert "Reset count: 3" in output
def test_cli_workspace_snapshot_prints_human_output(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def create_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot_name == "checkpoint"
return {
"workspace_id": workspace_id,
"snapshot": {
"snapshot_name": snapshot_name,
"kind": "named",
"entry_count": 3,
"bytes_written": 42,
},
}
def list_snapshots(self, workspace_id: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
return {
"workspace_id": workspace_id,
"count": 2,
"snapshots": [
{
"snapshot_name": "baseline",
"kind": "baseline",
"entry_count": 1,
"bytes_written": 10,
"deletable": False,
},
{
"snapshot_name": "checkpoint",
"kind": "named",
"entry_count": 3,
"bytes_written": 42,
"deletable": True,
},
],
}
def delete_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
assert workspace_id == "workspace-123"
assert snapshot_name == "checkpoint"
return {
"workspace_id": workspace_id,
"snapshot_name": snapshot_name,
"deleted": True,
}
class SnapshotCreateParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="create",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotCreateParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
cli.main()
create_output = capsys.readouterr().out
assert "[workspace-snapshot-create] workspace_id=workspace-123" in create_output
assert "snapshot_name=checkpoint kind=named" in create_output
class SnapshotListParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="list",
workspace_id="workspace-123",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotListParser())
cli.main()
list_output = capsys.readouterr().out
assert "baseline [baseline]" in list_output
assert "checkpoint [named]" in list_output
class SnapshotDeleteParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="delete",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: SnapshotDeleteParser())
cli.main()
delete_output = capsys.readouterr().out
assert "Deleted workspace snapshot: checkpoint" in delete_output
def test_cli_workspace_snapshot_error_paths(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
def create_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
del workspace_id, snapshot_name
raise RuntimeError("create boom")
def list_snapshots(self, workspace_id: str) -> dict[str, Any]:
del workspace_id
raise RuntimeError("list boom")
def delete_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
del workspace_id, snapshot_name
raise RuntimeError("delete boom")
def _run(args: argparse.Namespace) -> tuple[str, str]:
class StubParser:
def parse_args(self) -> argparse.Namespace:
return args
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
with pytest.raises(SystemExit, match="1"):
cli.main()
captured = capsys.readouterr()
return captured.out, captured.err
out, err = _run(
argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="create",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=True,
)
)
assert json.loads(out)["error"] == "create boom"
assert err == ""
out, err = _run(
argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="list",
workspace_id="workspace-123",
json=False,
)
)
assert out == ""
assert "[error] list boom" in err
out, err = _run(
argparse.Namespace(
command="workspace",
workspace_command="snapshot",
workspace_snapshot_command="delete",
workspace_id="workspace-123",
snapshot_name="checkpoint",
json=False,
)
)
assert out == ""
assert "[error] delete boom" in err
def test_cli_workspace_sync_push_prints_json(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None: