feat: snapshot/restore checkpoints (SAND-WP-0007)

Add workspace checkpoint API with SnapshotStore, extension hooks on
compose-ssh and saas-stub, manager orchestration, CLI/HTTP surface,
profile.compose-checkpoint, and docs/tests.
This commit is contained in:
tegwick 2026-06-24 07:57:40 +02:00
parent 2760ef2373
commit 952cebf2e9
21 changed files with 966 additions and 34 deletions

View file

@ -5,7 +5,12 @@ from __future__ import annotations
from fastapi import FastAPI, HTTPException
from sandboxer.core.manager import SandboxManager
from sandboxer.models import SandboxCreateRequest, SandboxStatus
from sandboxer.models import (
SandboxCreateRequest,
SandboxStatus,
SnapshotRecord,
SnapshotRestoreRequest,
)
app = FastAPI(title="sand-boxer", version="0.0.0")
_manager = SandboxManager()
@ -37,4 +42,44 @@ def destroy_sandbox(sandbox_id: str) -> SandboxStatus:
try:
return _manager.destroy(sandbox_id)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/v1/sandboxes/{sandbox_id}/snapshot", response_model=SnapshotRecord)
def snapshot_sandbox(
sandbox_id: str,
name: str | None = None,
) -> SnapshotRecord:
try:
return _manager.snapshot(sandbox_id, name=name)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/v1/snapshots/{snapshot_id}/restore", response_model=SandboxStatus)
def restore_snapshot(
snapshot_id: str,
request: SnapshotRestoreRequest | None = None,
) -> SandboxStatus:
req = request or SnapshotRestoreRequest()
try:
return _manager.restore(snapshot_id, host=req.host, consumer=req.consumer)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except (ValueError, Exception) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/v1/snapshots", response_model=list[SnapshotRecord])
def list_snapshots(sandbox_id: str | None = None) -> list[SnapshotRecord]:
return _manager.list_snapshots(sandbox_id=sandbox_id)
@app.get("/v1/snapshots/{snapshot_id}", response_model=SnapshotRecord)
def get_snapshot(snapshot_id: str) -> SnapshotRecord:
record = _manager.get_snapshot(snapshot_id)
if not record:
raise HTTPException(status_code=404, detail="snapshot not found")
return record