diff --git a/cli.py b/cli.py index a39cabc..f24be32 100644 --- a/cli.py +++ b/cli.py @@ -423,7 +423,7 @@ def _cmd_revoke(args: argparse.Namespace) -> int: return 0 -def _cmd_status(args: argparse.Namespace | None = None) -> int: +def _cmd_status() -> int: """Show whether the WSS server is listening on the configured port. Probes the port from the active config via a TCP socket handshake @@ -446,7 +446,7 @@ def _cmd_status(args: argparse.Namespace | None = None) -> int: return 1 -def _cmd_restart(args: argparse.Namespace | None = None) -> int: +def _cmd_restart() -> int: """POST /admin/restart to drain and restart the WSS server in-place. The server re-reads the internal auth token from diff --git a/errors.py b/errors.py index 7f06e7a..4f82ae4 100644 --- a/errors.py +++ b/errors.py @@ -24,68 +24,8 @@ class TokenStoreError(PluginError): """Token store read/write/decrypt failure.""" -class AuthError(PluginError): - """Authentication failure on the WSS server (bad token, wrong node name).""" - - -class NodeNotConnectedError(PluginError): - """A call was made to a node that's not currently in the registry. - - Surfaces to the caller of :meth:`NodeEnvironment.execute` (and - its sibling ``read``/``write`` methods) when the target node - name is unknown or its WebSocket has dropped. Distinct from - :class:`NodeExecutionError` (the node is fine, but the *call* - failed) and from :class:`asyncio.TimeoutError` (the node is - fine, but the call didn't return in time). - """ - - -class NodeExecutionError(PluginError): - """A node returned a structured ``exec_result`` with status=error. - - Carries the protocol-level reason and code so callers (Agent) - can decide whether to retry, surface a user-visible message, or - fall back to a different node. The node connection itself - stays open; the failure is per-call. - - Attributes: - code: The protocol error code (e.g. 3001 for - ``exec_timeout``). May be 0 for shape violations the - server couldn't categorise — the ``str(exc)`` message - is the authoritative description in that case. - """ - - def __init__(self, message: str, *, code: int = 0) -> None: - super().__init__(message) - self.code = code - - -class NodeReadError(PluginError): - """A node returned a structured ``read_result`` or ``write_result`` with status=error. - - Covers the file-I/O surface (``read``/``write`` in PROTOCOL - §3.8-3.11). One exception type for both directions keeps the - tool layer's error handling simple — the agent doesn't need - to distinguish a read failure from a write failure at the - catch site; the message says which. - - Attributes: - code: The protocol error code mapped from the node's - ``error`` string (PROTOCOL §4). May be 0 for shape - violations the server couldn't categorise. - """ - - def __init__(self, message: str, *, code: int = 0) -> None: - super().__init__(message) - self.code = code - - __all__ = [ "PluginError", "ConfigError", "TokenStoreError", - "AuthError", - "NodeNotConnectedError", - "NodeExecutionError", - "NodeReadError", ] diff --git a/lifecycle.py b/lifecycle.py index bdf6c79..ab133c2 100644 --- a/lifecycle.py +++ b/lifecycle.py @@ -12,9 +12,7 @@ * :class:`ServerRunner` — owns the uvicorn.Server instance, the background asyncio task, and the FastAPI app. - * :func:`get_default_runner` / :func:`reset_default_runner` — module - singletons used by :func:`hermes_nodes_plugin.register` to attach - a single runner to the gateway's session lifecycle. + * :func:`get_default_runner` — module singleton set by * The two thin hook callbacks the plugin's ``register(ctx)`` binds: they translate Hermes session events into ``runner.start`` / ``runner.drain`` calls. @@ -490,8 +488,7 @@ async def _sweep_stale_connections(self) -> None: # The plugin's ``register(ctx)`` binds a *singleton* runner: there's -# one per Hermes process. Tests call :func:`reset_default_runner` to -# start each test from a clean slate. The runner is constructed +# one per Hermes process. The runner is constructed # lazily on the first ``start()`` so the "Fernet key missing" error # is deferred to startup, not to plugin load. _default_runner: ServerRunner | None = None diff --git a/server.py b/server.py index a77ebd3..c99316e 100644 --- a/server.py +++ b/server.py @@ -22,14 +22,13 @@ PROTOCOL_MAJOR, _ensure_internal_token, _internal_token_path, + _read_token_from_disk, create_app, - run_server, ) from .wsserver.server import _safe_close __all__ = [ "create_app", - "run_server", "CLOSE_AUTH_FAILED", "CLOSE_PROTOCOL_VERSION", "CLOSE_MESSAGE_OUT_OF_ORDER", @@ -37,5 +36,6 @@ "PROTOCOL_MAJOR", "_ensure_internal_token", "_internal_token_path", + "_read_token_from_disk", "_safe_close", ] diff --git a/tools.py b/tools.py index 6f3f90f..6d328bb 100644 --- a/tools.py +++ b/tools.py @@ -24,7 +24,7 @@ def handler(args, **kw) -> str: import time from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import Any logger = logging.getLogger(__name__) @@ -33,9 +33,6 @@ def handler(args, **kw) -> str: DEFAULT_EXEC_TIMEOUT_SECONDS = 60.0 MAX_FILE_BYTES = 10 * 1024 * 1024 # 10 MiB -if TYPE_CHECKING: - from .registry import NodeConnection, NodeRegistry - # --------------------------------------------------------------------------- # Retry helper @@ -172,7 +169,6 @@ def _node_exec_impl( cwd: str | None = None, env: dict[str, str] | None = None, timeout_ms: int | None = None, - registry: "NodeRegistry | None" = None, ) -> str: """Run ``command`` on the named node. @@ -238,7 +234,6 @@ def _node_read_impl( path: str, *, timeout_ms: int | None = None, - registry: "NodeRegistry | None" = None, ) -> str: """Read a file from the named node.""" if not target: @@ -299,7 +294,6 @@ def _node_write_impl( *, mode: str = "overwrite", timeout_ms: int | None = None, - registry: "NodeRegistry | None" = None, ) -> str: """Write text to a file on the named node.""" if not target: @@ -351,10 +345,7 @@ def _node_write_impl( return json.dumps({"error": f"node_write failed: {e}"}) -def _node_list_impl( - *, - registry: "NodeRegistry | None" = None, -) -> str: +def _node_list_impl() -> str: """List paired nodes with their current connection state. Hits the HTTP /nodes/status endpoint. This is always queried over @@ -417,7 +408,6 @@ def node_exec(args: dict, **kw: Any) -> str: cwd=args.get("cwd"), env=args.get("env"), timeout_ms=args.get("timeout_ms"), - registry=args.get("registry"), ) @@ -427,7 +417,6 @@ def node_read(args: dict, **kw: Any) -> str: target=args.get("target"), path=args.get("path"), timeout_ms=args.get("timeout_ms"), - registry=args.get("registry"), ) @@ -439,47 +428,12 @@ def node_write(args: dict, **kw: Any) -> str: content=args.get("content"), mode=args.get("mode", "overwrite"), timeout_ms=args.get("timeout_ms"), - registry=args.get("registry"), ) def node_list(args: dict, **kw: Any) -> str: """Hermes tool handler — dispatches to _node_list_impl.""" - return _node_list_impl(registry=args.get("registry")) - - -# --------------------------------------------------------------------------- -# Internal helpers (kept for API compatibility with existing callers) -# --------------------------------------------------------------------------- - - -def _resolve_registry(override: "NodeRegistry | None") -> "NodeRegistry": - """Return ``override`` if given, else a fresh registry. - - Note: the tools now use httpx WebSocket to talk to the local server - directly, so this function is no longer used for exec/read/write. - It is kept for API compatibility with any external callers that - pass an explicit registry. - """ - if override is not None: - return override - from .registry import NodeRegistry - - return NodeRegistry() - - -def _connection_summary(conn: "NodeConnection") -> dict[str, Any]: - """Render a :class:`NodeConnection` as a JSON-serialisable dict.""" - return { - "name": conn.name, - "connected": True, - "connected_at": conn.connected_at.isoformat(), - "last_heartbeat": conn.last_heartbeat.isoformat() - if conn.last_heartbeat is not None - else None, - "session_id": conn.session_id, - "remote_addr": conn.remote_addr, - } + return _node_list_impl() # Public symbols. diff --git a/wsserver/__init__.py b/wsserver/__init__.py index e897000..f6f8f2c 100644 --- a/wsserver/__init__.py +++ b/wsserver/__init__.py @@ -8,6 +8,6 @@ result handling and the waiter/future completion logic). """ -from .server import create_app, run_server +from .server import create_app -__all__ = ["create_app", "run_server"] +__all__ = ["create_app"] diff --git a/wsserver/server.py b/wsserver/server.py index 7390192..d92a8a5 100644 --- a/wsserver/server.py +++ b/wsserver/server.py @@ -842,49 +842,16 @@ async def admin_restart( return app -# --------------------------------------------------------------------------- -# Convenience: run the server with uvicorn -# --------------------------------------------------------------------------- - - -def run_server( - config: NodeServerConfig, - *, - token_store: TokenStore | None = None, - registry: NodeRegistry | None = None, -) -> None: - """Run the WSS server on ``config.host:config.port`` until interrupted. - - Production entry point. Test code should call :func:`create_app` - and drive the ASGI app through ``httpx`` / ``websockets`` instead - of binding a real socket. - """ - import uvicorn - - app = create_app( - token_store=token_store, registry=registry, config=config - ) - ssl_kwargs: dict[str, Any] = {} - if config.uses_tls(): - ssl_kwargs = { - "ssl_certfile": config.tls_cert_path, - "ssl_keyfile": config.tls_key_path, - } - uvicorn.run( - app, - host=config.host, - port=config.port, - log_level="info", - **ssl_kwargs, - ) - - __all__ = [ "create_app", - "run_server", "CLOSE_AUTH_FAILED", "CLOSE_PROTOCOL_VERSION", "CLOSE_MESSAGE_OUT_OF_ORDER", "CLOSE_RATE_LIMIT_EXCEEDED", + "CLOSE_HANDSHAKE_TIMEOUT", "PROTOCOL_MAJOR", + "_ensure_internal_token", + "_internal_token_path", + "_safe_close", + "_read_token_from_disk", ]