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:
parent
535efc6919
commit
899a6760c4
25 changed files with 1658 additions and 58 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue