Add host bootstrap and repair helpers

Add a dedicated pyro host surface for supported chat hosts so Claude Code, Codex, and OpenCode users can connect or repair the canonical MCP setup without hand-writing raw commands or config edits.

Implement the shared host helper layer and wire it through the CLI with connect, print-config, doctor, and repair, all generated from the same canonical pyro mcp serve command shape and project-source flags. Update the docs, public contract, examples, changelog, and roadmap so the helper flow becomes the primary onramp while raw host-specific commands remain as reference material.

Harden the verification path that this milestone exposed: temp git repos in tests now disable commit signing, socket-based port tests skip cleanly when the sandbox forbids those primitives, and make test still uses multiple cores by default but caps xdist workers to a stable value so make check stays fast and deterministic here.

Validation:
- uv lock
- UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make check
- UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make dist-check
This commit is contained in:
Thales Maciel 2026-03-13 16:46:10 -03:00
parent 535efc6919
commit 899a6760c4
25 changed files with 1658 additions and 58 deletions

View file

@ -15,6 +15,13 @@ import pytest
from pyro_mcp import workspace_ports
def _socketpair_or_skip() -> tuple[socket.socket, socket.socket]:
try:
return socket.socketpair()
except PermissionError as exc:
pytest.skip(f"socketpair unavailable in this environment: {exc}")
class _EchoHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
data = self.request.recv(65536)
@ -50,18 +57,26 @@ def test_workspace_port_proxy_handler_ignores_upstream_connect_failure(
def test_workspace_port_proxy_forwards_tcp_traffic() -> None:
upstream = socketserver.ThreadingTCPServer(
(workspace_ports.DEFAULT_PUBLISHED_PORT_HOST, 0),
_EchoHandler,
)
try:
upstream = socketserver.ThreadingTCPServer(
(workspace_ports.DEFAULT_PUBLISHED_PORT_HOST, 0),
_EchoHandler,
)
except PermissionError as exc:
pytest.skip(f"TCP bind unavailable in this environment: {exc}")
upstream_thread = threading.Thread(target=upstream.serve_forever, daemon=True)
upstream_thread.start()
upstream_host = str(upstream.server_address[0])
upstream_port = int(upstream.server_address[1])
proxy = workspace_ports._ProxyServer( # noqa: SLF001
(workspace_ports.DEFAULT_PUBLISHED_PORT_HOST, 0),
(upstream_host, upstream_port),
)
try:
proxy = workspace_ports._ProxyServer( # noqa: SLF001
(workspace_ports.DEFAULT_PUBLISHED_PORT_HOST, 0),
(upstream_host, upstream_port),
)
except PermissionError as exc:
upstream.shutdown()
upstream.server_close()
pytest.skip(f"proxy TCP bind unavailable in this environment: {exc}")
proxy_thread = threading.Thread(target=proxy.serve_forever, daemon=True)
proxy_thread.start()
try:
@ -202,8 +217,8 @@ def test_workspace_ports_main_shutdown_handler_stops_server(
def test_workspace_port_proxy_handler_handles_empty_and_invalid_selector_events(
monkeypatch: Any,
) -> None:
source, source_peer = socket.socketpair()
upstream, upstream_peer = socket.socketpair()
source, source_peer = _socketpair_or_skip()
upstream, upstream_peer = _socketpair_or_skip()
source_peer.close()
class FakeSelector:
@ -246,10 +261,17 @@ def test_workspace_port_proxy_handler_handles_recv_and_send_errors(
monkeypatch: Any,
) -> None:
def _run_once(*, close_source: bool) -> None:
source, source_peer = socket.socketpair()
upstream, upstream_peer = socket.socketpair()
source, source_peer = _socketpair_or_skip()
upstream, upstream_peer = _socketpair_or_skip()
if not close_source:
source_peer.sendall(b"hello")
try:
source_peer.sendall(b"hello")
except PermissionError as exc:
source.close()
source_peer.close()
upstream.close()
upstream_peer.close()
pytest.skip(f"socket send unavailable in this environment: {exc}")
class FakeSelector:
def register(self, *_args: Any, **_kwargs: Any) -> None: