Add workspace naming and discovery

Make concurrent workspaces easier to rediscover and resume without relying on opaque IDs alone.

Add optional workspace names, key/value labels, workspace list, and workspace update across the CLI, Python SDK, and MCP surface, and persist last_activity_at so list ordering reflects real mutating activity.

Update the stable contract, install/first-run docs, roadmap, and Python workspace example to teach the new discovery flow, and validate it with focused manager/CLI/API/server coverage plus uv lock, make check, make dist-check, and a real multi-workspace smoke for create, list, update, exec, reorder, and delete.
This commit is contained in:
Thales Maciel 2026-03-12 23:16:10 -03:00
parent ab02ae46c7
commit 446f7fce04
21 changed files with 999 additions and 23 deletions

View file

@ -50,6 +50,8 @@ def test_pyro_create_server_registers_vm_run(tmp_path: Path) -> None:
assert "vm_run" in tool_names
assert "vm_create" in tool_names
assert "workspace_create" in tool_names
assert "workspace_list" in tool_names
assert "workspace_update" in tool_names
assert "workspace_start" in tool_names
assert "workspace_stop" in tool_names
assert "workspace_diff" in tool_names
@ -140,6 +142,14 @@ def test_pyro_workspace_network_policy_and_published_ports_delegate() -> None:
calls.append(("create_workspace", kwargs))
return {"workspace_id": "workspace-123"}
def list_workspaces(self) -> dict[str, Any]:
calls.append(("list_workspaces", {}))
return {"count": 1, "workspaces": [{"workspace_id": "workspace-123"}]}
def update_workspace(self, workspace_id: str, **kwargs: Any) -> dict[str, Any]:
calls.append(("update_workspace", {"workspace_id": workspace_id, **kwargs}))
return {"workspace_id": workspace_id, "name": "repro-fix", "labels": {"owner": "codex"}}
def start_service(
self,
workspace_id: str,
@ -163,6 +173,15 @@ def test_pyro_workspace_network_policy_and_published_ports_delegate() -> None:
pyro.create_workspace(
environment="debian:12",
network_policy="egress+published-ports",
name="repro-fix",
labels={"issue": "123"},
)
pyro.list_workspaces()
pyro.update_workspace(
"workspace-123",
name="repro-fix",
labels={"owner": "codex"},
clear_labels=["issue"],
)
pyro.start_service(
"workspace-123",
@ -182,9 +201,25 @@ def test_pyro_workspace_network_policy_and_published_ports_delegate() -> None:
"allow_host_compat": False,
"seed_path": None,
"secrets": None,
"name": "repro-fix",
"labels": {"issue": "123"},
},
)
assert calls[1] == (
"list_workspaces",
{},
)
assert calls[2] == (
"update_workspace",
{
"workspace_id": "workspace-123",
"name": "repro-fix",
"clear_name": False,
"labels": {"owner": "codex"},
"clear_labels": ["issue"],
},
)
assert calls[3] == (
"start_service",
{
"workspace_id": "workspace-123",
@ -219,12 +254,20 @@ def test_pyro_workspace_methods_delegate_to_manager(tmp_path: Path) -> None:
environment="debian:12-base",
allow_host_compat=True,
seed_path=source_dir,
name="repro-fix",
labels={"issue": "123"},
secrets=[
{"name": "API_TOKEN", "value": "expected"},
{"name": "FILE_TOKEN", "file_path": str(secret_file)},
],
)
workspace_id = str(created["workspace_id"])
listed_before = pyro.list_workspaces()
updated_metadata = pyro.update_workspace(
workspace_id,
labels={"owner": "codex"},
clear_labels=["issue"],
)
updated_dir = tmp_path / "updated"
updated_dir.mkdir()
(updated_dir / "more.txt").write_text("more\n", encoding="utf-8")
@ -293,6 +336,11 @@ def test_pyro_workspace_methods_delegate_to_manager(tmp_path: Path) -> None:
{"name": "API_TOKEN", "source_kind": "literal"},
{"name": "FILE_TOKEN", "source_kind": "file"},
]
assert created["name"] == "repro-fix"
assert created["labels"] == {"issue": "123"}
assert listed_before["count"] == 1
assert listed_before["workspaces"][0]["name"] == "repro-fix"
assert updated_metadata["labels"] == {"owner": "codex"}
assert executed["stdout"] == "[REDACTED]\n"
assert any(entry["path"] == "/workspace/note.txt" for entry in listed_files["entries"])
assert file_read["content"] == "ok\n"