Add workspace service lifecycle with typed readiness
Make persistent workspaces capable of running long-lived background processes instead of forcing everything through one-shot exec calls. Add workspace service start/list/status/logs/stop across the CLI, Python SDK, and MCP server, with multiple named services per workspace, typed readiness probes (file, tcp, http, and command), and aggregate service counts on workspace status. Keep service state and logs outside /workspace so diff and export semantics stay workspace-scoped, and extend the guest agent plus backends to persist service records and logs across separate calls. Update the 2.7.0 docs, examples, changelog, and roadmap milestone to reflect the shipped surface. Validation: uv lock; UV_CACHE_DIR=.uv-cache make check; UV_CACHE_DIR=.uv-cache make dist-check; real guest-backed Firecracker smoke for workspace create, two service starts, list/status/logs, diff unaffected, stop, and delete.
This commit is contained in:
parent
84a7e18d4d
commit
f504f0a331
28 changed files with 4098 additions and 124 deletions
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
|
|
@ -42,6 +41,11 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
|
|||
assert "shell_write" in tool_names
|
||||
assert "shell_signal" in tool_names
|
||||
assert "shell_close" in tool_names
|
||||
assert "service_start" in tool_names
|
||||
assert "service_list" in tool_names
|
||||
assert "service_status" in tool_names
|
||||
assert "service_logs" in tool_names
|
||||
assert "service_stop" in tool_names
|
||||
|
||||
|
||||
def test_vm_run_round_trip(tmp_path: Path) -> None:
|
||||
|
|
@ -192,20 +196,7 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
raise TypeError("expected structured dictionary result")
|
||||
return cast(dict[str, Any], structured)
|
||||
|
||||
async def _run() -> tuple[
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
dict[str, Any],
|
||||
]:
|
||||
async def _run() -> tuple[dict[str, Any], ...]:
|
||||
server = create_server(manager=manager)
|
||||
created = _extract_structured(
|
||||
await server.call_tool(
|
||||
|
|
@ -254,57 +245,45 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
},
|
||||
)
|
||||
)
|
||||
opened = _extract_structured(
|
||||
await server.call_tool("shell_open", {"workspace_id": workspace_id})
|
||||
)
|
||||
shell_id = str(opened["shell_id"])
|
||||
written = _extract_structured(
|
||||
service = _extract_structured(
|
||||
await server.call_tool(
|
||||
"shell_write",
|
||||
"service_start",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"shell_id": shell_id,
|
||||
"input": "pwd",
|
||||
"service_name": "app",
|
||||
"command": "sh -lc 'touch .ready; while true; do sleep 60; done'",
|
||||
"ready_file": ".ready",
|
||||
},
|
||||
)
|
||||
)
|
||||
read = _extract_structured(
|
||||
services = _extract_structured(
|
||||
await server.call_tool("service_list", {"workspace_id": workspace_id})
|
||||
)
|
||||
service_status = _extract_structured(
|
||||
await server.call_tool(
|
||||
"shell_read",
|
||||
"service_status",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"shell_id": shell_id,
|
||||
"service_name": "app",
|
||||
},
|
||||
)
|
||||
)
|
||||
deadline = time.time() + 5
|
||||
while "/workspace" not in str(read["output"]) and time.time() < deadline:
|
||||
read = _extract_structured(
|
||||
await server.call_tool(
|
||||
"shell_read",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"shell_id": shell_id,
|
||||
"cursor": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(0.05)
|
||||
signaled = _extract_structured(
|
||||
service_logs = _extract_structured(
|
||||
await server.call_tool(
|
||||
"shell_signal",
|
||||
"service_logs",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"shell_id": shell_id,
|
||||
"service_name": "app",
|
||||
"all": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
closed = _extract_structured(
|
||||
service_stopped = _extract_structured(
|
||||
await server.call_tool(
|
||||
"shell_close",
|
||||
"service_stop",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"shell_id": shell_id,
|
||||
"service_name": "app",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
@ -320,11 +299,11 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
executed,
|
||||
diffed,
|
||||
exported,
|
||||
opened,
|
||||
written,
|
||||
read,
|
||||
signaled,
|
||||
closed,
|
||||
service,
|
||||
services,
|
||||
service_status,
|
||||
service_logs,
|
||||
service_stopped,
|
||||
logs,
|
||||
deleted,
|
||||
)
|
||||
|
|
@ -335,11 +314,11 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
executed,
|
||||
diffed,
|
||||
exported,
|
||||
opened,
|
||||
written,
|
||||
read,
|
||||
signaled,
|
||||
closed,
|
||||
service,
|
||||
services,
|
||||
service_status,
|
||||
service_logs,
|
||||
service_stopped,
|
||||
logs,
|
||||
deleted,
|
||||
) = asyncio.run(_run())
|
||||
|
|
@ -350,10 +329,10 @@ def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
|||
assert diffed["changed"] is True
|
||||
assert exported["artifact_type"] == "file"
|
||||
assert Path(str(exported["output_path"])).read_text(encoding="utf-8") == "more\n"
|
||||
assert opened["state"] == "running"
|
||||
assert written["input_length"] == 3
|
||||
assert "/workspace" in read["output"]
|
||||
assert signaled["signal"] == "INT"
|
||||
assert closed["closed"] is True
|
||||
assert service["state"] == "running"
|
||||
assert services["count"] == 1
|
||||
assert service_status["state"] == "running"
|
||||
assert service_logs["tail_lines"] is None
|
||||
assert service_stopped["state"] == "stopped"
|
||||
assert logs["count"] == 1
|
||||
assert deleted["deleted"] is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue