Add stopped-workspace disk export and inspection
Finish the 3.1.0 secondary disk-tools milestone so stable workspaces can be stopped, inspected offline, exported as raw ext4 images, and started again without changing the primary workspace-first interaction model. Add workspace stop/start plus workspace disk export/list/read across the CLI, SDK, and MCP, backed by a new offline debugfs inspection helper and guest-only validation. Scrub runtime-only guest state before disk inspection/export, and fix the real guest reliability gaps by flushing the filesystem on stop and removing stale Firecracker socket files before restart. Update the docs, examples, changelog, and roadmap to mark 3.1.0 done, and cover the new lifecycle/disk paths with API, CLI, manager, contract, and package-surface tests. Validation: uv lock; UV_CACHE_DIR=.uv-cache make check; UV_CACHE_DIR=.uv-cache make dist-check; real guest-backed smoke for create, shell/service activity, stop, workspace disk list/read/export, start, exec, and delete.
This commit is contained in:
parent
f2d20ef30a
commit
287f6d100f
26 changed files with 2585 additions and 34 deletions
|
|
@ -119,6 +119,12 @@ class Pyro:
|
|||
def status_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
return self._manager.status_workspace(workspace_id)
|
||||
|
||||
def stop_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
return self._manager.stop_workspace(workspace_id)
|
||||
|
||||
def start_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
return self._manager.start_workspace(workspace_id)
|
||||
|
||||
def push_workspace_sync(
|
||||
self,
|
||||
workspace_id: str,
|
||||
|
|
@ -151,6 +157,43 @@ class Pyro:
|
|||
def diff_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
return self._manager.diff_workspace(workspace_id)
|
||||
|
||||
def export_workspace_disk(
|
||||
self,
|
||||
workspace_id: str,
|
||||
*,
|
||||
output_path: str | Path,
|
||||
) -> dict[str, Any]:
|
||||
return self._manager.export_workspace_disk(
|
||||
workspace_id,
|
||||
output_path=output_path,
|
||||
)
|
||||
|
||||
def list_workspace_disk(
|
||||
self,
|
||||
workspace_id: str,
|
||||
*,
|
||||
path: str = "/workspace",
|
||||
recursive: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return self._manager.list_workspace_disk(
|
||||
workspace_id,
|
||||
path=path,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
def read_workspace_disk(
|
||||
self,
|
||||
workspace_id: str,
|
||||
path: str,
|
||||
*,
|
||||
max_bytes: int = 65536,
|
||||
) -> dict[str, Any]:
|
||||
return self._manager.read_workspace_disk(
|
||||
workspace_id,
|
||||
path=path,
|
||||
max_bytes=max_bytes,
|
||||
)
|
||||
|
||||
def create_snapshot(self, workspace_id: str, snapshot_name: str) -> dict[str, Any]:
|
||||
return self._manager.create_snapshot(workspace_id, snapshot_name)
|
||||
|
||||
|
|
@ -457,6 +500,16 @@ class Pyro:
|
|||
"""Inspect workspace state and latest command metadata."""
|
||||
return self.status_workspace(workspace_id)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_stop(workspace_id: str) -> dict[str, Any]:
|
||||
"""Stop one persistent workspace without resetting `/workspace`."""
|
||||
return self.stop_workspace(workspace_id)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_start(workspace_id: str) -> dict[str, Any]:
|
||||
"""Start one stopped persistent workspace without resetting `/workspace`."""
|
||||
return self.start_workspace(workspace_id)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_logs(workspace_id: str) -> dict[str, Any]:
|
||||
"""Return persisted command history for one workspace."""
|
||||
|
|
@ -476,6 +529,40 @@ class Pyro:
|
|||
"""Compare `/workspace` to the immutable create-time baseline."""
|
||||
return self.diff_workspace(workspace_id)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_disk_export(
|
||||
workspace_id: str,
|
||||
output_path: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Export the raw stopped workspace rootfs image to one host path."""
|
||||
return self.export_workspace_disk(workspace_id, output_path=output_path)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_disk_list(
|
||||
workspace_id: str,
|
||||
path: str = "/workspace",
|
||||
recursive: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Inspect one stopped workspace rootfs path without booting the guest."""
|
||||
return self.list_workspace_disk(
|
||||
workspace_id,
|
||||
path=path,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
async def workspace_disk_read(
|
||||
workspace_id: str,
|
||||
path: str,
|
||||
max_bytes: int = 65536,
|
||||
) -> dict[str, Any]:
|
||||
"""Read one regular file from a stopped workspace rootfs without booting the guest."""
|
||||
return self.read_workspace_disk(
|
||||
workspace_id,
|
||||
path,
|
||||
max_bytes=max_bytes,
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
async def snapshot_create(workspace_id: str, snapshot_name: str) -> dict[str, Any]:
|
||||
"""Create one named workspace snapshot from the current `/workspace` tree."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue