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:
parent
dc86d84e96
commit
d0cf6d8f21
33 changed files with 1034 additions and 274 deletions
|
|
@ -8,7 +8,12 @@ from typing import Any, Literal, cast
|
|||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from pyro_mcp.contract import (
|
||||
PUBLIC_MCP_COLD_START_MODE_TOOLS,
|
||||
PUBLIC_MCP_INSPECT_MODE_TOOLS,
|
||||
PUBLIC_MCP_MODES,
|
||||
PUBLIC_MCP_PROFILES,
|
||||
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,
|
||||
|
|
@ -30,12 +35,77 @@ from pyro_mcp.vm_manager import (
|
|||
)
|
||||
|
||||
McpToolProfile = Literal["vm-run", "workspace-core", "workspace-full"]
|
||||
WorkspaceUseCaseMode = Literal["repro-fix", "inspect", "cold-start", "review-eval"]
|
||||
|
||||
_PROFILE_TOOLS: dict[str, tuple[str, ...]] = {
|
||||
"vm-run": PUBLIC_MCP_VM_RUN_PROFILE_TOOLS,
|
||||
"workspace-core": PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS,
|
||||
"workspace-full": PUBLIC_MCP_WORKSPACE_FULL_PROFILE_TOOLS,
|
||||
}
|
||||
_MODE_TOOLS: dict[str, tuple[str, ...]] = {
|
||||
"repro-fix": PUBLIC_MCP_REPRO_FIX_MODE_TOOLS,
|
||||
"inspect": PUBLIC_MCP_INSPECT_MODE_TOOLS,
|
||||
"cold-start": PUBLIC_MCP_COLD_START_MODE_TOOLS,
|
||||
"review-eval": PUBLIC_MCP_REVIEW_EVAL_MODE_TOOLS,
|
||||
}
|
||||
_MODE_CREATE_INTENT: dict[str, str] = {
|
||||
"repro-fix": "to reproduce a failure, patch files, rerun, diff, export, and reset",
|
||||
"inspect": "to inspect suspicious or unfamiliar code with the smallest persistent surface",
|
||||
"cold-start": (
|
||||
"to validate a fresh repo, keep one service alive, and export a "
|
||||
"validation report"
|
||||
),
|
||||
"review-eval": "to review interactively, checkpoint work, and export the final report",
|
||||
}
|
||||
_MODE_TOOL_DESCRIPTIONS: dict[str, dict[str, str]] = {
|
||||
"repro-fix": {
|
||||
"workspace_file_read": "Read one workspace file while investigating the broken state.",
|
||||
"workspace_file_write": "Write one workspace file directly as part of the fix loop.",
|
||||
"workspace_patch_apply": "Apply a structured text patch inside the workspace fix loop.",
|
||||
"workspace_export": "Export the fixed result or patch-ready file back to the host.",
|
||||
"workspace_summary": (
|
||||
"Summarize the current repro/fix session for review before export "
|
||||
"or reset."
|
||||
),
|
||||
},
|
||||
"inspect": {
|
||||
"workspace_file_list": "List suspicious files under the current workspace path.",
|
||||
"workspace_file_read": "Read one suspicious or unfamiliar workspace file.",
|
||||
"workspace_export": (
|
||||
"Export only the inspection report or artifact you chose to "
|
||||
"materialize."
|
||||
),
|
||||
"workspace_summary": "Summarize the current inspection session and its exported results.",
|
||||
},
|
||||
"cold-start": {
|
||||
"workspace_create": "Create and start a persistent workspace for cold-start validation.",
|
||||
"workspace_export": "Export the validation report or other final host-visible result.",
|
||||
"workspace_summary": (
|
||||
"Summarize the current validation session, service state, and "
|
||||
"exports."
|
||||
),
|
||||
"service_start": "Start a named validation or app service and wait for typed readiness.",
|
||||
"service_list": "List running and exited validation services in the current workspace.",
|
||||
"service_status": "Inspect one validation service and its readiness outcome.",
|
||||
"service_logs": "Read stdout and stderr from one validation service.",
|
||||
"service_stop": "Stop one validation service in the workspace.",
|
||||
},
|
||||
"review-eval": {
|
||||
"workspace_create": (
|
||||
"Create and start a persistent workspace for interactive review "
|
||||
"or evaluation."
|
||||
),
|
||||
"workspace_summary": "Summarize the current review session before exporting or resetting.",
|
||||
"shell_open": "Open an interactive review shell inside the workspace.",
|
||||
"shell_read": "Read chat-friendly PTY output from the current review shell.",
|
||||
"shell_write": "Send one line of input to the current review shell.",
|
||||
"shell_signal": "Interrupt or terminate the current review shell process group.",
|
||||
"shell_close": "Close the current review shell.",
|
||||
"snapshot_create": "Create a checkpoint before the review branch diverges.",
|
||||
"snapshot_list": "List the baseline plus named review checkpoints.",
|
||||
"snapshot_delete": "Delete one named review checkpoint.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _validate_mcp_profile(profile: str) -> McpToolProfile:
|
||||
|
|
@ -45,16 +115,42 @@ def _validate_mcp_profile(profile: str) -> McpToolProfile:
|
|||
return cast(McpToolProfile, profile)
|
||||
|
||||
|
||||
def _workspace_create_description(startup_source: ProjectStartupSource | None) -> str:
|
||||
def _validate_workspace_mode(mode: str) -> WorkspaceUseCaseMode:
|
||||
if mode not in PUBLIC_MCP_MODES:
|
||||
expected = ", ".join(PUBLIC_MCP_MODES)
|
||||
raise ValueError(f"unknown workspace mode {mode!r}; expected one of: {expected}")
|
||||
return cast(WorkspaceUseCaseMode, mode)
|
||||
|
||||
|
||||
def _workspace_create_description(
|
||||
startup_source: ProjectStartupSource | None,
|
||||
*,
|
||||
mode: WorkspaceUseCaseMode | None = None,
|
||||
) -> str:
|
||||
if mode is not None:
|
||||
prefix = (
|
||||
"Create and start a persistent workspace "
|
||||
f"{_MODE_CREATE_INTENT[mode]}."
|
||||
)
|
||||
else:
|
||||
prefix = "Create and start a persistent workspace."
|
||||
if startup_source is None:
|
||||
return "Create and start a persistent workspace."
|
||||
return prefix
|
||||
described_source = describe_project_startup_source(startup_source)
|
||||
if described_source is None:
|
||||
return "Create and start a persistent workspace."
|
||||
return (
|
||||
"Create and start a persistent workspace. If `seed_path` is omitted, "
|
||||
f"the server seeds from {described_source}."
|
||||
)
|
||||
return prefix
|
||||
return f"{prefix} If `seed_path` is omitted, the server seeds from {described_source}."
|
||||
|
||||
|
||||
def _tool_description(
|
||||
tool_name: str,
|
||||
*,
|
||||
mode: WorkspaceUseCaseMode | None,
|
||||
fallback: str,
|
||||
) -> str:
|
||||
if mode is None:
|
||||
return fallback
|
||||
return _MODE_TOOL_DESCRIPTIONS.get(mode, {}).get(tool_name, fallback)
|
||||
|
||||
|
||||
class Pyro:
|
||||
|
|
@ -487,6 +583,7 @@ class Pyro:
|
|||
self,
|
||||
*,
|
||||
profile: McpToolProfile = "workspace-core",
|
||||
mode: WorkspaceUseCaseMode | None = None,
|
||||
project_path: str | Path | None = None,
|
||||
repo_url: str | None = None,
|
||||
repo_ref: str | None = None,
|
||||
|
|
@ -502,13 +599,20 @@ class Pyro:
|
|||
`repo_url`, and `no_project_source` override that behavior explicitly.
|
||||
"""
|
||||
normalized_profile = _validate_mcp_profile(profile)
|
||||
normalized_mode = _validate_workspace_mode(mode) if mode is not None else None
|
||||
if normalized_mode is not None and normalized_profile != "workspace-core":
|
||||
raise ValueError("mode and profile are mutually exclusive")
|
||||
startup_source = resolve_project_startup_source(
|
||||
project_path=project_path,
|
||||
repo_url=repo_url,
|
||||
repo_ref=repo_ref,
|
||||
no_project_source=no_project_source,
|
||||
)
|
||||
enabled_tools = set(_PROFILE_TOOLS[normalized_profile])
|
||||
enabled_tools = set(
|
||||
_MODE_TOOLS[normalized_mode]
|
||||
if normalized_mode is not None
|
||||
else _PROFILE_TOOLS[normalized_profile]
|
||||
)
|
||||
server = FastMCP(name="pyro_mcp")
|
||||
|
||||
def _enabled(tool_name: str) -> bool:
|
||||
|
|
@ -621,7 +725,10 @@ class Pyro:
|
|||
return self.reap_expired()
|
||||
|
||||
if _enabled("workspace_create"):
|
||||
workspace_create_description = _workspace_create_description(startup_source)
|
||||
workspace_create_description = _workspace_create_description(
|
||||
startup_source,
|
||||
mode=normalized_mode,
|
||||
)
|
||||
|
||||
def _create_workspace_from_server_defaults(
|
||||
*,
|
||||
|
|
@ -668,7 +775,7 @@ class Pyro:
|
|||
_prepared_seed=prepared_seed,
|
||||
)
|
||||
|
||||
if normalized_profile == "workspace-core":
|
||||
if normalized_mode is not None or normalized_profile == "workspace-core":
|
||||
|
||||
@server.tool(name="workspace_create", description=workspace_create_description)
|
||||
async def workspace_create_core(
|
||||
|
|
@ -749,15 +856,21 @@ class Pyro:
|
|||
)
|
||||
|
||||
if _enabled("workspace_exec"):
|
||||
if normalized_profile == "workspace-core":
|
||||
if normalized_mode is not None or normalized_profile == "workspace-core":
|
||||
|
||||
@server.tool(name="workspace_exec")
|
||||
@server.tool(
|
||||
name="workspace_exec",
|
||||
description=_tool_description(
|
||||
"workspace_exec",
|
||||
mode=normalized_mode,
|
||||
fallback="Run one command inside an existing persistent workspace.",
|
||||
),
|
||||
)
|
||||
async def workspace_exec_core(
|
||||
workspace_id: str,
|
||||
command: str,
|
||||
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
"""Run one command inside an existing persistent workspace."""
|
||||
return self.exec_workspace(
|
||||
workspace_id,
|
||||
command=command,
|
||||
|
|
@ -767,14 +880,20 @@ class Pyro:
|
|||
|
||||
else:
|
||||
|
||||
@server.tool(name="workspace_exec")
|
||||
@server.tool(
|
||||
name="workspace_exec",
|
||||
description=_tool_description(
|
||||
"workspace_exec",
|
||||
mode=normalized_mode,
|
||||
fallback="Run one command inside an existing persistent workspace.",
|
||||
),
|
||||
)
|
||||
async def workspace_exec_full(
|
||||
workspace_id: str,
|
||||
command: str,
|
||||
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
|
||||
secret_env: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run one command inside an existing persistent workspace."""
|
||||
return self.exec_workspace(
|
||||
workspace_id,
|
||||
command=command,
|
||||
|
|
@ -823,20 +942,30 @@ class Pyro:
|
|||
|
||||
if _enabled("workspace_summary"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_summary",
|
||||
mode=normalized_mode,
|
||||
fallback="Summarize the current workspace session for human review.",
|
||||
)
|
||||
)
|
||||
async def workspace_summary(workspace_id: str) -> dict[str, Any]:
|
||||
"""Summarize the current workspace session for human review."""
|
||||
return self.summarize_workspace(workspace_id)
|
||||
|
||||
if _enabled("workspace_export"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_export",
|
||||
mode=normalized_mode,
|
||||
fallback="Export one file or directory from `/workspace` back to the host.",
|
||||
)
|
||||
)
|
||||
async def workspace_export(
|
||||
workspace_id: str,
|
||||
path: str,
|
||||
output_path: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Export one file or directory from `/workspace` back to the host."""
|
||||
return self.export_workspace(workspace_id, path, output_path=output_path)
|
||||
|
||||
if _enabled("workspace_diff"):
|
||||
|
|
@ -848,13 +977,21 @@ class Pyro:
|
|||
|
||||
if _enabled("workspace_file_list"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_file_list",
|
||||
mode=normalized_mode,
|
||||
fallback=(
|
||||
"List metadata for files and directories under one "
|
||||
"live workspace path."
|
||||
),
|
||||
)
|
||||
)
|
||||
async def workspace_file_list(
|
||||
workspace_id: str,
|
||||
path: str = "/workspace",
|
||||
recursive: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List metadata for files and directories under one live workspace path."""
|
||||
return self.list_workspace_files(
|
||||
workspace_id,
|
||||
path=path,
|
||||
|
|
@ -863,13 +1000,18 @@ class Pyro:
|
|||
|
||||
if _enabled("workspace_file_read"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_file_read",
|
||||
mode=normalized_mode,
|
||||
fallback="Read one regular text file from a live workspace path.",
|
||||
)
|
||||
)
|
||||
async def workspace_file_read(
|
||||
workspace_id: str,
|
||||
path: str,
|
||||
max_bytes: int = 65536,
|
||||
) -> dict[str, Any]:
|
||||
"""Read one regular text file from a live workspace path."""
|
||||
return self.read_workspace_file(
|
||||
workspace_id,
|
||||
path,
|
||||
|
|
@ -878,13 +1020,18 @@ class Pyro:
|
|||
|
||||
if _enabled("workspace_file_write"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_file_write",
|
||||
mode=normalized_mode,
|
||||
fallback="Create or replace one regular text file under `/workspace`.",
|
||||
)
|
||||
)
|
||||
async def workspace_file_write(
|
||||
workspace_id: str,
|
||||
path: str,
|
||||
text: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Create or replace one regular text file under `/workspace`."""
|
||||
return self.write_workspace_file(
|
||||
workspace_id,
|
||||
path,
|
||||
|
|
@ -893,12 +1040,17 @@ class Pyro:
|
|||
|
||||
if _enabled("workspace_patch_apply"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"workspace_patch_apply",
|
||||
mode=normalized_mode,
|
||||
fallback="Apply a unified text patch inside one live workspace.",
|
||||
)
|
||||
)
|
||||
async def workspace_patch_apply(
|
||||
workspace_id: str,
|
||||
patch: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Apply a unified text patch inside one live workspace."""
|
||||
return self.apply_workspace_patch(
|
||||
workspace_id,
|
||||
patch=patch,
|
||||
|
|
@ -976,27 +1128,62 @@ class Pyro:
|
|||
return self.reset_workspace(workspace_id, snapshot=snapshot)
|
||||
|
||||
if _enabled("shell_open"):
|
||||
if normalized_mode == "review-eval":
|
||||
|
||||
@server.tool()
|
||||
async def shell_open(
|
||||
workspace_id: str,
|
||||
cwd: str = "/workspace",
|
||||
cols: int = 120,
|
||||
rows: int = 30,
|
||||
secret_env: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Open a persistent interactive shell inside one workspace."""
|
||||
return self.open_shell(
|
||||
workspace_id,
|
||||
cwd=cwd,
|
||||
cols=cols,
|
||||
rows=rows,
|
||||
secret_env=secret_env,
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_open",
|
||||
mode=normalized_mode,
|
||||
fallback="Open a persistent interactive shell inside one workspace.",
|
||||
)
|
||||
)
|
||||
async def shell_open(
|
||||
workspace_id: str,
|
||||
cwd: str = "/workspace",
|
||||
cols: int = 120,
|
||||
rows: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
return self.open_shell(
|
||||
workspace_id,
|
||||
cwd=cwd,
|
||||
cols=cols,
|
||||
rows=rows,
|
||||
secret_env=None,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_open",
|
||||
mode=normalized_mode,
|
||||
fallback="Open a persistent interactive shell inside one workspace.",
|
||||
)
|
||||
)
|
||||
async def shell_open(
|
||||
workspace_id: str,
|
||||
cwd: str = "/workspace",
|
||||
cols: int = 120,
|
||||
rows: int = 30,
|
||||
secret_env: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return self.open_shell(
|
||||
workspace_id,
|
||||
cwd=cwd,
|
||||
cols=cols,
|
||||
rows=rows,
|
||||
secret_env=secret_env,
|
||||
)
|
||||
|
||||
if _enabled("shell_read"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_read",
|
||||
mode=normalized_mode,
|
||||
fallback="Read merged PTY output from a workspace shell.",
|
||||
)
|
||||
)
|
||||
async def shell_read(
|
||||
workspace_id: str,
|
||||
shell_id: str,
|
||||
|
|
@ -1005,7 +1192,6 @@ class Pyro:
|
|||
plain: bool = False,
|
||||
wait_for_idle_ms: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Read merged PTY output from a workspace shell."""
|
||||
return self.read_shell(
|
||||
workspace_id,
|
||||
shell_id,
|
||||
|
|
@ -1017,14 +1203,19 @@ class Pyro:
|
|||
|
||||
if _enabled("shell_write"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_write",
|
||||
mode=normalized_mode,
|
||||
fallback="Write text input to a persistent workspace shell.",
|
||||
)
|
||||
)
|
||||
async def shell_write(
|
||||
workspace_id: str,
|
||||
shell_id: str,
|
||||
input: str,
|
||||
append_newline: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Write text input to a persistent workspace shell."""
|
||||
return self.write_shell(
|
||||
workspace_id,
|
||||
shell_id,
|
||||
|
|
@ -1034,13 +1225,18 @@ class Pyro:
|
|||
|
||||
if _enabled("shell_signal"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_signal",
|
||||
mode=normalized_mode,
|
||||
fallback="Send a signal to the shell process group.",
|
||||
)
|
||||
)
|
||||
async def shell_signal(
|
||||
workspace_id: str,
|
||||
shell_id: str,
|
||||
signal_name: str = "INT",
|
||||
) -> dict[str, Any]:
|
||||
"""Send a signal to the shell process group."""
|
||||
return self.signal_shell(
|
||||
workspace_id,
|
||||
shell_id,
|
||||
|
|
@ -1049,74 +1245,142 @@ class Pyro:
|
|||
|
||||
if _enabled("shell_close"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"shell_close",
|
||||
mode=normalized_mode,
|
||||
fallback="Close a persistent workspace shell.",
|
||||
)
|
||||
)
|
||||
async def shell_close(workspace_id: str, shell_id: str) -> dict[str, Any]:
|
||||
"""Close a persistent workspace shell."""
|
||||
return self.close_shell(workspace_id, shell_id)
|
||||
|
||||
if _enabled("service_start"):
|
||||
if normalized_mode == "cold-start":
|
||||
|
||||
@server.tool()
|
||||
async def service_start(
|
||||
workspace_id: str,
|
||||
service_name: str,
|
||||
command: str,
|
||||
cwd: str = "/workspace",
|
||||
ready_file: str | None = None,
|
||||
ready_tcp: str | None = None,
|
||||
ready_http: str | None = None,
|
||||
ready_command: str | None = None,
|
||||
ready_timeout_seconds: int = 30,
|
||||
ready_interval_ms: int = 500,
|
||||
secret_env: dict[str, str] | None = None,
|
||||
published_ports: list[dict[str, int | None]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Start a named long-running service inside a workspace."""
|
||||
readiness: dict[str, Any] | None = None
|
||||
if ready_file is not None:
|
||||
readiness = {"type": "file", "path": ready_file}
|
||||
elif ready_tcp is not None:
|
||||
readiness = {"type": "tcp", "address": ready_tcp}
|
||||
elif ready_http is not None:
|
||||
readiness = {"type": "http", "url": ready_http}
|
||||
elif ready_command is not None:
|
||||
readiness = {"type": "command", "command": ready_command}
|
||||
return self.start_service(
|
||||
workspace_id,
|
||||
service_name,
|
||||
command=command,
|
||||
cwd=cwd,
|
||||
readiness=readiness,
|
||||
ready_timeout_seconds=ready_timeout_seconds,
|
||||
ready_interval_ms=ready_interval_ms,
|
||||
secret_env=secret_env,
|
||||
published_ports=published_ports,
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_start",
|
||||
mode=normalized_mode,
|
||||
fallback="Start a named long-running service inside a workspace.",
|
||||
)
|
||||
)
|
||||
async def service_start(
|
||||
workspace_id: str,
|
||||
service_name: str,
|
||||
command: str,
|
||||
cwd: str = "/workspace",
|
||||
ready_file: str | None = None,
|
||||
ready_tcp: str | None = None,
|
||||
ready_http: str | None = None,
|
||||
ready_command: str | None = None,
|
||||
ready_timeout_seconds: int = 30,
|
||||
ready_interval_ms: int = 500,
|
||||
) -> dict[str, Any]:
|
||||
readiness: dict[str, Any] | None = None
|
||||
if ready_file is not None:
|
||||
readiness = {"type": "file", "path": ready_file}
|
||||
elif ready_tcp is not None:
|
||||
readiness = {"type": "tcp", "address": ready_tcp}
|
||||
elif ready_http is not None:
|
||||
readiness = {"type": "http", "url": ready_http}
|
||||
elif ready_command is not None:
|
||||
readiness = {"type": "command", "command": ready_command}
|
||||
return self.start_service(
|
||||
workspace_id,
|
||||
service_name,
|
||||
command=command,
|
||||
cwd=cwd,
|
||||
readiness=readiness,
|
||||
ready_timeout_seconds=ready_timeout_seconds,
|
||||
ready_interval_ms=ready_interval_ms,
|
||||
secret_env=None,
|
||||
published_ports=None,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_start",
|
||||
mode=normalized_mode,
|
||||
fallback="Start a named long-running service inside a workspace.",
|
||||
)
|
||||
)
|
||||
async def service_start(
|
||||
workspace_id: str,
|
||||
service_name: str,
|
||||
command: str,
|
||||
cwd: str = "/workspace",
|
||||
ready_file: str | None = None,
|
||||
ready_tcp: str | None = None,
|
||||
ready_http: str | None = None,
|
||||
ready_command: str | None = None,
|
||||
ready_timeout_seconds: int = 30,
|
||||
ready_interval_ms: int = 500,
|
||||
secret_env: dict[str, str] | None = None,
|
||||
published_ports: list[dict[str, int | None]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
readiness: dict[str, Any] | None = None
|
||||
if ready_file is not None:
|
||||
readiness = {"type": "file", "path": ready_file}
|
||||
elif ready_tcp is not None:
|
||||
readiness = {"type": "tcp", "address": ready_tcp}
|
||||
elif ready_http is not None:
|
||||
readiness = {"type": "http", "url": ready_http}
|
||||
elif ready_command is not None:
|
||||
readiness = {"type": "command", "command": ready_command}
|
||||
return self.start_service(
|
||||
workspace_id,
|
||||
service_name,
|
||||
command=command,
|
||||
cwd=cwd,
|
||||
readiness=readiness,
|
||||
ready_timeout_seconds=ready_timeout_seconds,
|
||||
ready_interval_ms=ready_interval_ms,
|
||||
secret_env=secret_env,
|
||||
published_ports=published_ports,
|
||||
)
|
||||
|
||||
if _enabled("service_list"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_list",
|
||||
mode=normalized_mode,
|
||||
fallback="List named services in one workspace.",
|
||||
)
|
||||
)
|
||||
async def service_list(workspace_id: str) -> dict[str, Any]:
|
||||
"""List named services in one workspace."""
|
||||
return self.list_services(workspace_id)
|
||||
|
||||
if _enabled("service_status"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_status",
|
||||
mode=normalized_mode,
|
||||
fallback="Inspect one named workspace service.",
|
||||
)
|
||||
)
|
||||
async def service_status(workspace_id: str, service_name: str) -> dict[str, Any]:
|
||||
"""Inspect one named workspace service."""
|
||||
return self.status_service(workspace_id, service_name)
|
||||
|
||||
if _enabled("service_logs"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_logs",
|
||||
mode=normalized_mode,
|
||||
fallback="Read persisted stdout/stderr for one workspace service.",
|
||||
)
|
||||
)
|
||||
async def service_logs(
|
||||
workspace_id: str,
|
||||
service_name: str,
|
||||
tail_lines: int = 200,
|
||||
all: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Read persisted stdout/stderr for one workspace service."""
|
||||
return self.logs_service(
|
||||
workspace_id,
|
||||
service_name,
|
||||
|
|
@ -1126,9 +1390,14 @@ class Pyro:
|
|||
|
||||
if _enabled("service_stop"):
|
||||
|
||||
@server.tool()
|
||||
@server.tool(
|
||||
description=_tool_description(
|
||||
"service_stop",
|
||||
mode=normalized_mode,
|
||||
fallback="Stop one running service in a workspace.",
|
||||
)
|
||||
)
|
||||
async def service_stop(workspace_id: str, service_name: str) -> dict[str, Any]:
|
||||
"""Stop one running service in a workspace."""
|
||||
return self.stop_service(workspace_id, service_name)
|
||||
|
||||
if _enabled("workspace_delete"):
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from textwrap import dedent
|
|||
from typing import Any, cast
|
||||
|
||||
from pyro_mcp import __version__
|
||||
from pyro_mcp.api import McpToolProfile, Pyro
|
||||
from pyro_mcp.contract import PUBLIC_MCP_PROFILES
|
||||
from pyro_mcp.api import McpToolProfile, Pyro, WorkspaceUseCaseMode
|
||||
from pyro_mcp.contract import PUBLIC_MCP_MODES, PUBLIC_MCP_PROFILES
|
||||
from pyro_mcp.demo import run_demo
|
||||
from pyro_mcp.host_helpers import (
|
||||
HostDoctorEntry,
|
||||
|
|
@ -181,6 +181,7 @@ def _build_host_server_config(args: argparse.Namespace) -> HostServerConfig:
|
|||
return HostServerConfig(
|
||||
installed_package=bool(getattr(args, "installed_package", False)),
|
||||
profile=cast(McpToolProfile, str(getattr(args, "profile", "workspace-core"))),
|
||||
mode=cast(WorkspaceUseCaseMode | None, getattr(args, "mode", None)),
|
||||
project_path=getattr(args, "project_path", None),
|
||||
repo_url=getattr(args, "repo_url", None),
|
||||
repo_ref=getattr(args, "repo_ref", None),
|
||||
|
|
@ -856,11 +857,17 @@ def _add_host_server_source_args(parser: argparse.ArgumentParser) -> None:
|
|||
action="store_true",
|
||||
help="Use `pyro mcp serve` instead of the default `uvx --from pyro-mcp pyro mcp serve`.",
|
||||
)
|
||||
parser.add_argument(
|
||||
profile_group = parser.add_mutually_exclusive_group()
|
||||
profile_group.add_argument(
|
||||
"--profile",
|
||||
choices=PUBLIC_MCP_PROFILES,
|
||||
default="workspace-core",
|
||||
help="Server profile to configure for the host helper flow.",
|
||||
help="Explicit profile for the host helper flow when not using a named mode.",
|
||||
)
|
||||
profile_group.add_argument(
|
||||
"--mode",
|
||||
choices=PUBLIC_MCP_MODES,
|
||||
help="Opinionated use-case mode for the host helper flow.",
|
||||
)
|
||||
source_group = parser.add_mutually_exclusive_group()
|
||||
source_group.add_argument(
|
||||
|
|
@ -1017,6 +1024,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro host connect claude-code
|
||||
pyro host connect claude-code --mode cold-start
|
||||
pyro host connect codex --project-path /abs/path/to/repo
|
||||
pyro host print-config opencode
|
||||
pyro host repair opencode
|
||||
|
|
@ -1037,7 +1045,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro host connect claude-code
|
||||
pyro host connect claude-code --mode cold-start
|
||||
pyro host connect codex --installed-package
|
||||
pyro host connect codex --mode repro-fix
|
||||
pyro host connect codex --project-path /abs/path/to/repo
|
||||
"""
|
||||
),
|
||||
|
|
@ -1061,6 +1071,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro host print-config opencode
|
||||
pyro host print-config opencode --mode repro-fix
|
||||
pyro host print-config opencode --output ./opencode.json
|
||||
pyro host print-config opencode --project-path /abs/path/to/repo
|
||||
"""
|
||||
|
|
@ -1089,6 +1100,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro host doctor
|
||||
pyro host doctor --mode inspect
|
||||
pyro host doctor --project-path /abs/path/to/repo
|
||||
pyro host doctor --installed-package
|
||||
"""
|
||||
|
|
@ -1112,6 +1124,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro host repair claude-code
|
||||
pyro host repair claude-code --mode review-eval
|
||||
pyro host repair codex --project-path /abs/path/to/repo
|
||||
pyro host repair opencode
|
||||
"""
|
||||
|
|
@ -1141,6 +1154,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"""
|
||||
Examples:
|
||||
pyro mcp serve
|
||||
pyro mcp serve --mode repro-fix
|
||||
pyro mcp serve --mode inspect
|
||||
pyro mcp serve --project-path .
|
||||
pyro mcp serve --repo-url https://github.com/example/project.git
|
||||
pyro mcp serve --profile vm-run
|
||||
|
|
@ -1155,19 +1170,27 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
help="Run the MCP server over stdio.",
|
||||
description=(
|
||||
"Expose pyro tools over stdio for an MCP client. Bare `pyro mcp "
|
||||
"serve` now starts `workspace-core`, the recommended first profile "
|
||||
"for most chat hosts. When launched from inside a Git checkout, it "
|
||||
"also seeds the first workspace from that repo by default."
|
||||
"serve` starts the generic `workspace-core` path. Use `--mode` to "
|
||||
"start from an opinionated use-case flow, or `--profile` to choose "
|
||||
"a generic profile directly. When launched from inside a Git "
|
||||
"checkout, it also seeds the first workspace from that repo by default."
|
||||
),
|
||||
epilog=dedent(
|
||||
"""
|
||||
Default and recommended first start:
|
||||
Generic default path:
|
||||
pyro mcp serve
|
||||
pyro mcp serve --project-path .
|
||||
pyro mcp serve --repo-url https://github.com/example/project.git
|
||||
|
||||
Named modes:
|
||||
repro-fix: structured edit / diff / export / reset loop
|
||||
inspect: smallest persistent inspection surface
|
||||
cold-start: validation plus service readiness
|
||||
review-eval: shell plus snapshots for review workflows
|
||||
|
||||
Profiles:
|
||||
workspace-core: default for normal persistent chat editing
|
||||
workspace-core: default for normal persistent chat editing and the
|
||||
recommended first profile for most chat hosts
|
||||
vm-run: smallest one-shot-only surface
|
||||
workspace-full: larger opt-in surface for shells, services,
|
||||
snapshots, secrets, network policy, and disk tools
|
||||
|
|
@ -1178,22 +1201,28 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
- use --project-path when the host does not preserve cwd
|
||||
- use --repo-url for a clean-clone source outside a local checkout
|
||||
|
||||
Use --profile workspace-full only when the host truly needs those
|
||||
extra workspace capabilities.
|
||||
Use --mode when one named use case already matches the job. Fall
|
||||
back to the generic no-mode path when the mode feels too narrow.
|
||||
"""
|
||||
),
|
||||
formatter_class=_HelpFormatter,
|
||||
)
|
||||
mcp_serve_parser.add_argument(
|
||||
mcp_profile_group = mcp_serve_parser.add_mutually_exclusive_group()
|
||||
mcp_profile_group.add_argument(
|
||||
"--profile",
|
||||
choices=PUBLIC_MCP_PROFILES,
|
||||
default="workspace-core",
|
||||
help=(
|
||||
"Expose only one model-facing tool profile. `workspace-core` is "
|
||||
"the default and recommended first profile for most chat hosts; "
|
||||
"`workspace-full` is the larger opt-in profile."
|
||||
"Expose one generic model-facing tool profile instead of a named mode. "
|
||||
"`workspace-core` is the generic default and `workspace-full` is the "
|
||||
"larger opt-in profile."
|
||||
),
|
||||
)
|
||||
mcp_profile_group.add_argument(
|
||||
"--mode",
|
||||
choices=PUBLIC_MCP_MODES,
|
||||
help="Expose one opinionated use-case mode instead of the generic profile path.",
|
||||
)
|
||||
mcp_source_group = mcp_serve_parser.add_mutually_exclusive_group()
|
||||
mcp_source_group.add_argument(
|
||||
"--project-path",
|
||||
|
|
@ -2794,6 +2823,7 @@ def main() -> None:
|
|||
if args.command == "mcp":
|
||||
pyro.create_server(
|
||||
profile=args.profile,
|
||||
mode=getattr(args, "mode", None),
|
||||
project_path=args.project_path,
|
||||
repo_url=args.repo_url,
|
||||
repo_ref=args.repo_ref,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ PUBLIC_CLI_ENV_SUBCOMMANDS = ("inspect", "list", "pull", "prune")
|
|||
PUBLIC_CLI_HOST_SUBCOMMANDS = ("connect", "doctor", "print-config", "repair")
|
||||
PUBLIC_CLI_HOST_COMMON_FLAGS = (
|
||||
"--installed-package",
|
||||
"--mode",
|
||||
"--profile",
|
||||
"--project-path",
|
||||
"--repo-url",
|
||||
|
|
@ -20,6 +21,7 @@ PUBLIC_CLI_HOST_PRINT_CONFIG_FLAGS = PUBLIC_CLI_HOST_COMMON_FLAGS + ("--output",
|
|||
PUBLIC_CLI_HOST_REPAIR_FLAGS = PUBLIC_CLI_HOST_COMMON_FLAGS + ("--config-path",)
|
||||
PUBLIC_CLI_MCP_SUBCOMMANDS = ("serve",)
|
||||
PUBLIC_CLI_MCP_SERVE_FLAGS = (
|
||||
"--mode",
|
||||
"--profile",
|
||||
"--project-path",
|
||||
"--repo-url",
|
||||
|
|
@ -140,6 +142,7 @@ PUBLIC_CLI_RUN_FLAGS = (
|
|||
"--json",
|
||||
)
|
||||
PUBLIC_MCP_PROFILES = ("vm-run", "workspace-core", "workspace-full")
|
||||
PUBLIC_MCP_MODES = ("repro-fix", "inspect", "cold-start", "review-eval")
|
||||
|
||||
PUBLIC_SDK_METHODS = (
|
||||
"apply_workspace_patch",
|
||||
|
|
@ -258,4 +261,77 @@ PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS = (
|
|||
"workspace_sync_push",
|
||||
"workspace_update",
|
||||
)
|
||||
PUBLIC_MCP_REPRO_FIX_MODE_TOOLS = (
|
||||
"workspace_create",
|
||||
"workspace_delete",
|
||||
"workspace_diff",
|
||||
"workspace_exec",
|
||||
"workspace_export",
|
||||
"workspace_file_list",
|
||||
"workspace_file_read",
|
||||
"workspace_file_write",
|
||||
"workspace_list",
|
||||
"workspace_logs",
|
||||
"workspace_patch_apply",
|
||||
"workspace_reset",
|
||||
"workspace_summary",
|
||||
"workspace_status",
|
||||
"workspace_sync_push",
|
||||
"workspace_update",
|
||||
)
|
||||
PUBLIC_MCP_INSPECT_MODE_TOOLS = (
|
||||
"workspace_create",
|
||||
"workspace_delete",
|
||||
"workspace_exec",
|
||||
"workspace_export",
|
||||
"workspace_file_list",
|
||||
"workspace_file_read",
|
||||
"workspace_list",
|
||||
"workspace_logs",
|
||||
"workspace_summary",
|
||||
"workspace_status",
|
||||
"workspace_update",
|
||||
)
|
||||
PUBLIC_MCP_COLD_START_MODE_TOOLS = (
|
||||
"service_list",
|
||||
"service_logs",
|
||||
"service_start",
|
||||
"service_status",
|
||||
"service_stop",
|
||||
"workspace_create",
|
||||
"workspace_delete",
|
||||
"workspace_exec",
|
||||
"workspace_export",
|
||||
"workspace_file_list",
|
||||
"workspace_file_read",
|
||||
"workspace_list",
|
||||
"workspace_logs",
|
||||
"workspace_reset",
|
||||
"workspace_summary",
|
||||
"workspace_status",
|
||||
"workspace_update",
|
||||
)
|
||||
PUBLIC_MCP_REVIEW_EVAL_MODE_TOOLS = (
|
||||
"shell_close",
|
||||
"shell_open",
|
||||
"shell_read",
|
||||
"shell_signal",
|
||||
"shell_write",
|
||||
"snapshot_create",
|
||||
"snapshot_delete",
|
||||
"snapshot_list",
|
||||
"workspace_create",
|
||||
"workspace_delete",
|
||||
"workspace_diff",
|
||||
"workspace_exec",
|
||||
"workspace_export",
|
||||
"workspace_file_list",
|
||||
"workspace_file_read",
|
||||
"workspace_list",
|
||||
"workspace_logs",
|
||||
"workspace_reset",
|
||||
"workspace_summary",
|
||||
"workspace_status",
|
||||
"workspace_update",
|
||||
)
|
||||
PUBLIC_MCP_WORKSPACE_FULL_PROFILE_TOOLS = PUBLIC_MCP_TOOLS
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from datetime import UTC, datetime
|
|||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pyro_mcp.api import McpToolProfile
|
||||
from pyro_mcp.api import McpToolProfile, WorkspaceUseCaseMode
|
||||
|
||||
SUPPORTED_HOST_CONNECT_TARGETS = ("claude-code", "codex")
|
||||
SUPPORTED_HOST_REPAIR_TARGETS = ("claude-code", "codex", "opencode")
|
||||
|
|
@ -26,6 +26,7 @@ HostStatus = Literal["drifted", "missing", "ok", "unavailable"]
|
|||
class HostServerConfig:
|
||||
installed_package: bool = False
|
||||
profile: McpToolProfile = "workspace-core"
|
||||
mode: WorkspaceUseCaseMode | None = None
|
||||
project_path: str | None = None
|
||||
repo_url: str | None = None
|
||||
repo_ref: str | None = None
|
||||
|
|
@ -60,6 +61,8 @@ def _host_binary(host: str) -> str:
|
|||
|
||||
|
||||
def _canonical_server_command(config: HostServerConfig) -> list[str]:
|
||||
if config.mode is not None and config.profile != "workspace-core":
|
||||
raise ValueError("--mode and --profile are mutually exclusive")
|
||||
if config.project_path is not None and config.repo_url is not None:
|
||||
raise ValueError("--project-path and --repo-url are mutually exclusive")
|
||||
if config.no_project_source and (
|
||||
|
|
@ -76,7 +79,9 @@ def _canonical_server_command(config: HostServerConfig) -> list[str]:
|
|||
command = ["pyro", "mcp", "serve"]
|
||||
if not config.installed_package:
|
||||
command = ["uvx", "--from", "pyro-mcp", *command]
|
||||
if config.profile != "workspace-core":
|
||||
if config.mode is not None:
|
||||
command.extend(["--mode", config.mode])
|
||||
elif config.profile != "workspace-core":
|
||||
command.extend(["--profile", config.profile])
|
||||
if config.project_path is not None:
|
||||
command.extend(["--project-path", config.project_path])
|
||||
|
|
@ -97,7 +102,9 @@ def _repair_command(host: str, config: HostServerConfig, *, config_path: Path |
|
|||
command = ["pyro", "host", "repair", host]
|
||||
if config.installed_package:
|
||||
command.append("--installed-package")
|
||||
if config.profile != "workspace-core":
|
||||
if config.mode is not None:
|
||||
command.extend(["--mode", config.mode])
|
||||
elif config.profile != "workspace-core":
|
||||
command.extend(["--profile", config.profile])
|
||||
if config.project_path is not None:
|
||||
command.extend(["--project-path", config.project_path])
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from pyro_mcp.api import McpToolProfile, Pyro
|
||||
from pyro_mcp.api import McpToolProfile, Pyro, WorkspaceUseCaseMode
|
||||
from pyro_mcp.vm_manager import VmManager
|
||||
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ def create_server(
|
|||
manager: VmManager | None = None,
|
||||
*,
|
||||
profile: McpToolProfile = "workspace-core",
|
||||
mode: WorkspaceUseCaseMode | None = None,
|
||||
project_path: str | Path | None = None,
|
||||
repo_url: str | None = None,
|
||||
repo_ref: str | None = None,
|
||||
|
|
@ -21,7 +22,8 @@ def create_server(
|
|||
) -> FastMCP:
|
||||
"""Create and return a configured MCP server instance.
|
||||
|
||||
`workspace-core` is the default stable chat-host profile in 4.x. Use
|
||||
Bare server creation uses the generic `workspace-core` path in 4.x. Use
|
||||
`mode=...` for one of the named use-case surfaces, or
|
||||
`profile="workspace-full"` only when the host truly needs the full
|
||||
advanced workspace surface. By default, the server auto-detects the
|
||||
nearest Git worktree root from its current working directory for
|
||||
|
|
@ -29,6 +31,7 @@ def create_server(
|
|||
"""
|
||||
return Pyro(manager=manager).create_server(
|
||||
profile=profile,
|
||||
mode=mode,
|
||||
project_path=project_path,
|
||||
repo_url=repo_url,
|
||||
repo_ref=repo_ref,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from typing import Any
|
|||
from pyro_mcp.runtime import DEFAULT_PLATFORM, RuntimePaths
|
||||
|
||||
DEFAULT_ENVIRONMENT_VERSION = "1.0.0"
|
||||
DEFAULT_CATALOG_VERSION = "4.3.0"
|
||||
DEFAULT_CATALOG_VERSION = "4.4.0"
|
||||
OCI_MANIFEST_ACCEPT = ", ".join(
|
||||
(
|
||||
"application/vnd.oci.image.index.v1+json",
|
||||
|
|
@ -48,7 +48,7 @@ class VmEnvironment:
|
|||
oci_repository: str | None = None
|
||||
oci_reference: str | None = None
|
||||
source_digest: str | None = None
|
||||
compatibility: str = ">=4.3.0,<5.0.0"
|
||||
compatibility: str = ">=4.4.0,<5.0.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ USE_CASE_CHOICES: Final[tuple[str, ...]] = USE_CASE_SCENARIOS + (USE_CASE_ALL_SC
|
|||
class WorkspaceUseCaseRecipe:
|
||||
scenario: str
|
||||
title: str
|
||||
profile: Literal["workspace-core", "workspace-full"]
|
||||
mode: Literal["repro-fix", "inspect", "cold-start", "review-eval"]
|
||||
smoke_target: str
|
||||
doc_path: str
|
||||
summary: str
|
||||
|
|
@ -39,7 +39,7 @@ WORKSPACE_USE_CASE_RECIPES: Final[tuple[WorkspaceUseCaseRecipe, ...]] = (
|
|||
WorkspaceUseCaseRecipe(
|
||||
scenario="cold-start-validation",
|
||||
title="Cold-Start Repo Validation",
|
||||
profile="workspace-full",
|
||||
mode="cold-start",
|
||||
smoke_target="smoke-cold-start-validation",
|
||||
doc_path="docs/use-cases/cold-start-repo-validation.md",
|
||||
summary=(
|
||||
|
|
@ -50,7 +50,7 @@ WORKSPACE_USE_CASE_RECIPES: Final[tuple[WorkspaceUseCaseRecipe, ...]] = (
|
|||
WorkspaceUseCaseRecipe(
|
||||
scenario="repro-fix-loop",
|
||||
title="Repro Plus Fix Loop",
|
||||
profile="workspace-core",
|
||||
mode="repro-fix",
|
||||
smoke_target="smoke-repro-fix-loop",
|
||||
doc_path="docs/use-cases/repro-fix-loop.md",
|
||||
summary=(
|
||||
|
|
@ -61,7 +61,7 @@ WORKSPACE_USE_CASE_RECIPES: Final[tuple[WorkspaceUseCaseRecipe, ...]] = (
|
|||
WorkspaceUseCaseRecipe(
|
||||
scenario="parallel-workspaces",
|
||||
title="Parallel Isolated Workspaces",
|
||||
profile="workspace-core",
|
||||
mode="repro-fix",
|
||||
smoke_target="smoke-parallel-workspaces",
|
||||
doc_path="docs/use-cases/parallel-workspaces.md",
|
||||
summary=(
|
||||
|
|
@ -72,7 +72,7 @@ WORKSPACE_USE_CASE_RECIPES: Final[tuple[WorkspaceUseCaseRecipe, ...]] = (
|
|||
WorkspaceUseCaseRecipe(
|
||||
scenario="untrusted-inspection",
|
||||
title="Unsafe Or Untrusted Code Inspection",
|
||||
profile="workspace-core",
|
||||
mode="inspect",
|
||||
smoke_target="smoke-untrusted-inspection",
|
||||
doc_path="docs/use-cases/untrusted-inspection.md",
|
||||
summary=(
|
||||
|
|
@ -83,7 +83,7 @@ WORKSPACE_USE_CASE_RECIPES: Final[tuple[WorkspaceUseCaseRecipe, ...]] = (
|
|||
WorkspaceUseCaseRecipe(
|
||||
scenario="review-eval",
|
||||
title="Review And Evaluation Workflows",
|
||||
profile="workspace-full",
|
||||
mode="review-eval",
|
||||
smoke_target="smoke-review-eval",
|
||||
doc_path="docs/use-cases/review-eval-workflows.md",
|
||||
summary=(
|
||||
|
|
@ -141,11 +141,12 @@ def _create_project_aware_workspace(
|
|||
*,
|
||||
environment: str,
|
||||
project_path: Path,
|
||||
mode: Literal["repro-fix", "cold-start"],
|
||||
name: str,
|
||||
labels: dict[str, str],
|
||||
) -> dict[str, object]:
|
||||
async def _run() -> dict[str, object]:
|
||||
server = pyro.create_server(profile="workspace-core", project_path=project_path)
|
||||
server = pyro.create_server(mode=mode, project_path=project_path)
|
||||
return _extract_structured_tool_result(
|
||||
await server.call_tool(
|
||||
"workspace_create",
|
||||
|
|
@ -194,14 +195,19 @@ def _scenario_cold_start_validation(pyro: Pyro, *, root: Path, environment: str)
|
|||
)
|
||||
workspace_id: str | None = None
|
||||
try:
|
||||
workspace_id = _create_workspace(
|
||||
created = _create_project_aware_workspace(
|
||||
pyro,
|
||||
environment=environment,
|
||||
seed_path=seed_dir,
|
||||
project_path=seed_dir,
|
||||
mode="cold-start",
|
||||
name="cold-start-validation",
|
||||
labels={"suite": USE_CASE_SUITE_LABEL, "use_case": "cold-start-validation"},
|
||||
)
|
||||
workspace_id = str(created["workspace_id"])
|
||||
_log(f"cold-start-validation workspace_id={workspace_id}")
|
||||
workspace_seed = created["workspace_seed"]
|
||||
assert isinstance(workspace_seed, dict), created
|
||||
assert workspace_seed["origin_kind"] == "project_path", created
|
||||
validation = pyro.exec_workspace(workspace_id, command="sh validate.sh")
|
||||
assert int(validation["exit_code"]) == 0, validation
|
||||
assert str(validation["stdout"]) == "validated\n", validation
|
||||
|
|
@ -259,6 +265,7 @@ def _scenario_repro_fix_loop(pyro: Pyro, *, root: Path, environment: str) -> Non
|
|||
pyro,
|
||||
environment=environment,
|
||||
project_path=seed_dir,
|
||||
mode="repro-fix",
|
||||
name="repro-fix-loop",
|
||||
labels={"suite": USE_CASE_SUITE_LABEL, "use_case": "repro-fix-loop"},
|
||||
)
|
||||
|
|
@ -495,7 +502,7 @@ def run_workspace_use_case_scenario(
|
|||
scenario_names = USE_CASE_SCENARIOS if scenario == USE_CASE_ALL_SCENARIO else (scenario,)
|
||||
for scenario_name in scenario_names:
|
||||
recipe = _RECIPE_BY_SCENARIO[scenario_name]
|
||||
_log(f"starting {recipe.scenario} ({recipe.title}) profile={recipe.profile}")
|
||||
_log(f"starting {recipe.scenario} ({recipe.title}) mode={recipe.mode}")
|
||||
scenario_root = root / scenario_name
|
||||
scenario_root.mkdir(parents=True, exist_ok=True)
|
||||
runner = _SCENARIO_RUNNERS[scenario_name]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue