Add opinionated MCP modes for workspace workflows

Introduce explicit repro-fix, inspect, cold-start, and review-eval modes across the MCP server, CLI, and host helpers, with canonical mode-to-tool mappings, narrowed schemas, and mode-specific tool descriptions on top of the existing workspace runtime.

Reposition the docs, host onramps, and use-case recipes so named modes are the primary user-facing startup story while the generic no-mode workspace-core path remains the escape hatch, and update the shared smoke runner to validate repro-fix and cold-start through mode-backed servers.

Validation: UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache uv run pytest --no-cov tests/test_api.py tests/test_server.py tests/test_host_helpers.py tests/test_public_contract.py tests/test_cli.py tests/test_workspace_use_case_smokes.py; UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make check; UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make dist-check; real guest-backed make smoke-repro-fix-loop smoke-cold-start-validation outside the sandbox.
This commit is contained in:
Thales Maciel 2026-03-13 20:00:35 -03:00
parent dc86d84e96
commit d0cf6d8f21
33 changed files with 1034 additions and 274 deletions

View file

@ -6,8 +6,14 @@ import time
from pathlib import Path
from typing import Any, cast
import pytest
from pyro_mcp.api import Pyro
from pyro_mcp.contract import (
PUBLIC_MCP_COLD_START_MODE_TOOLS,
PUBLIC_MCP_INSPECT_MODE_TOOLS,
PUBLIC_MCP_REPRO_FIX_MODE_TOOLS,
PUBLIC_MCP_REVIEW_EVAL_MODE_TOOLS,
PUBLIC_MCP_VM_RUN_PROFILE_TOOLS,
PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS,
PUBLIC_MCP_WORKSPACE_FULL_PROFILE_TOOLS,
@ -157,6 +163,120 @@ def test_pyro_create_server_workspace_core_profile_registers_expected_tools_and_
assert "workspace_disk_export" not in tool_map
def test_pyro_create_server_repro_fix_mode_registers_expected_tools_and_schemas(
tmp_path: Path,
) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
async def _run() -> tuple[list[str], dict[str, dict[str, Any]]]:
server = pyro.create_server(mode="repro-fix")
tools = await server.list_tools()
tool_map = {tool.name: tool.model_dump() for tool in tools}
return sorted(tool_map), tool_map
tool_names, tool_map = asyncio.run(_run())
assert tuple(tool_names) == tuple(sorted(PUBLIC_MCP_REPRO_FIX_MODE_TOOLS))
create_properties = tool_map["workspace_create"]["inputSchema"]["properties"]
assert "network_policy" not in create_properties
assert "secrets" not in create_properties
exec_properties = tool_map["workspace_exec"]["inputSchema"]["properties"]
assert "secret_env" not in exec_properties
assert "service_start" not in tool_map
assert "shell_open" not in tool_map
assert "snapshot_create" not in tool_map
assert "reproduce a failure" in str(tool_map["workspace_create"]["description"])
def test_pyro_create_server_cold_start_mode_registers_expected_tools_and_schemas(
tmp_path: Path,
) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
async def _run() -> tuple[list[str], dict[str, dict[str, Any]]]:
server = pyro.create_server(mode="cold-start")
tools = await server.list_tools()
tool_map = {tool.name: tool.model_dump() for tool in tools}
return sorted(tool_map), tool_map
tool_names, tool_map = asyncio.run(_run())
assert tuple(tool_names) == tuple(sorted(PUBLIC_MCP_COLD_START_MODE_TOOLS))
assert "shell_open" not in tool_map
assert "snapshot_create" not in tool_map
service_start_properties = tool_map["service_start"]["inputSchema"]["properties"]
assert "secret_env" not in service_start_properties
assert "published_ports" not in service_start_properties
create_properties = tool_map["workspace_create"]["inputSchema"]["properties"]
assert "network_policy" not in create_properties
def test_pyro_create_server_review_eval_mode_registers_expected_tools_and_schemas(
tmp_path: Path,
) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
async def _run() -> tuple[list[str], dict[str, dict[str, Any]]]:
server = pyro.create_server(mode="review-eval")
tools = await server.list_tools()
tool_map = {tool.name: tool.model_dump() for tool in tools}
return sorted(tool_map), tool_map
tool_names, tool_map = asyncio.run(_run())
assert tuple(tool_names) == tuple(sorted(PUBLIC_MCP_REVIEW_EVAL_MODE_TOOLS))
assert "service_start" not in tool_map
assert "shell_open" in tool_map
assert "snapshot_create" in tool_map
shell_open_properties = tool_map["shell_open"]["inputSchema"]["properties"]
assert "secret_env" not in shell_open_properties
def test_pyro_create_server_inspect_mode_registers_expected_tools(tmp_path: Path) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
async def _run() -> list[str]:
server = pyro.create_server(mode="inspect")
tools = await server.list_tools()
return sorted(tool.name for tool in tools)
assert tuple(asyncio.run(_run())) == tuple(sorted(PUBLIC_MCP_INSPECT_MODE_TOOLS))
def test_pyro_create_server_rejects_mode_and_non_default_profile(tmp_path: Path) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
with pytest.raises(ValueError, match="mutually exclusive"):
pyro.create_server(profile="workspace-full", mode="repro-fix")
def test_pyro_create_server_project_path_updates_workspace_create_description_and_default_seed(
tmp_path: Path,
) -> None: