From 6dcbcd0277a2d5cf42c53a8a013f7ac18f931fa1 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:33:34 -0700 Subject: [PATCH] refactor(console): remove hosted-context command blocking from Hermes Console (#66144) The dashboard console previously ran under a 'hosted' context that blocked most commands (auth add, config set model.*, mcp add --command, cron --script, ...) behind an allowlist + line-policy layer. With the full Hermes CLI now built into the dashboard, that policy layer is redundant gatekeeping: the console gets the same command surface everywhere. Removed: - ConsoleContext/contexts plumbing on ConsoleCommand + engine - EXPECTED_HOSTED_PATHS allowlist + _mark_hosted - _enforce_hosted_line_policy + HOSTED_CONFIG_* allow/block tables - _dashboard_console_context() and the context field on the ready frame - hosted-context tests; context badge in HermesConsoleModal Kept (mechanical, not policy): shell-syntax rejection, the interactive/server command blocks (gateway, dashboard, mcp serve, ...), mutating-command confirmations, output caps, and command timeouts. --- hermes_cli/console_engine.py | 296 ++---------------- hermes_cli/web_server.py | 15 +- tests/hermes_cli/test_console_engine.py | 162 ---------- .../hermes_cli/test_web_server_console_ws.py | 15 - web/src/components/HermesConsoleModal.tsx | 5 - 5 files changed, 26 insertions(+), 467 deletions(-) diff --git a/hermes_cli/console_engine.py b/hermes_cli/console_engine.py index 7bfa13fbf60..3806ee509dd 100644 --- a/hermes_cli/console_engine.py +++ b/hermes_cli/console_engine.py @@ -19,15 +19,11 @@ import sys from dataclasses import dataclass, replace from pathlib import Path from typing import Callable, Iterable, Literal, NoReturn, Sequence -from urllib.parse import urlparse from tools.ansi_strip import strip_ansi as _strip_ansi ConsoleStatus = Literal["ok", "error", "confirm_required", "exit", "clear"] -ConsoleContext = Literal["local", "hosted"] -ALL_CONTEXTS: frozenset[ConsoleContext] = frozenset({"local", "hosted"}) -LOCAL_CONTEXTS: frozenset[ConsoleContext] = frozenset({"local"}) class ConsoleCommandError(RuntimeError): @@ -50,7 +46,6 @@ class ConsoleCommand: handler: Callable[["HermesConsoleEngine", list[str]], str] mutating: bool = False confirmation: str = "" - contexts: frozenset[ConsoleContext] = LOCAL_CONTEXTS class _ArgumentParser(argparse.ArgumentParser): @@ -151,89 +146,6 @@ def _format_job(job: dict, action: str) -> str: return f"{action} job: {name} ({job_id}) [{state}]" -EXPECTED_HOSTED_PATHS: tuple[tuple[str, ...], ...] = ( - ("status",), - ("doctor",), - ("logs",), - ("version",), - ("prompt-size",), - ("insights",), - ("security", "audit"), - ("portal", "info"), - ("portal", "tools"), - ("send",), - ("config", "show"), - ("config", "path"), - ("config", "env-path"), - ("config", "check"), - ("config", "migrate"), - ("config", "set"), - ("sessions", "list"), - ("sessions", "stats"), - ("sessions", "export"), - ("sessions", "rename"), - ("sessions", "optimize"), - ("sessions", "repair"), - ("cron", "list"), - ("cron", "status"), - ("cron", "create"), - ("cron", "edit"), - ("cron", "pause"), - ("cron", "resume"), - ("cron", "run"), - ("cron", "remove"), - ("cron", "tick"), - ("profile",), - ("profile", "list"), - ("profile", "show"), - ("profile", "info"), - ("tools", "list"), - ("tools", "enable"), - ("tools", "disable"), - ("tools", "post-setup"), - ("skills", "browse"), - ("skills", "search"), - ("skills", "inspect"), - ("skills", "list"), - ("skills", "check"), - ("skills", "list-modified"), - ("skills", "diff"), - ("skills", "install"), - ("skills", "update"), - ("skills", "audit"), - ("skills", "uninstall"), - ("skills", "reset"), - ("skills", "opt-in"), - ("skills", "opt-out"), - ("skills", "repair-official"), - ("skills", "snapshot", "export"), - ("skills", "tap", "list"), - ("mcp", "list"), - ("mcp", "catalog"), - ("mcp", "test"), - ("mcp", "add"), - ("mcp", "remove"), - ("mcp", "install"), - ("mcp", "login"), - ("mcp", "reauth"), - ("mcp", "configure"), - ("mcp", "picker"), - ("memory", "status"), - ("auth", "list"), - ("auth", "status"), - ("auth", "reset"), - ("auth", "spotify", "status"), - ("pairing", "list"), - ("pairing", "approve"), - ("pairing", "revoke"), - ("pairing", "clear-pending"), - ("webhook", "list"), - ("webhook", "subscribe"), - ("webhook", "remove"), - ("webhook", "test"), -) - - def _parser_root() -> tuple[_ArgumentParser, argparse._SubParsersAction]: parser = _ArgumentParser(prog="hermes", add_help=False) subparsers = parser.add_subparsers(dest="_console_command") @@ -377,8 +289,7 @@ def _dispatch_extracted_subcommand( module_name: str, builder_name: str, main_handler_name: str, - console_context: ConsoleContext, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> str: parser, subparsers = _parser_root() module = importlib.import_module(module_name) @@ -388,7 +299,7 @@ def _dispatch_extracted_subcommand( builder(subparsers, **{main_handler_name: main_handler}) namespace = parser.parse_args([root, *fixed, *args]) if namespace_update: - namespace_update(namespace, console_context) + namespace_update(namespace) return _capture_output(lambda: _invoke_namespace(namespace)) @@ -400,8 +311,7 @@ def _dispatch_registered_subcommand( module_name: str, register_name: str, handler_name: str | None = None, - console_context: ConsoleContext, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> str: parser, subparsers = _parser_root() module = importlib.import_module(module_name) @@ -412,7 +322,7 @@ def _dispatch_registered_subcommand( top_parser.set_defaults(func=getattr(module, handler_name)) namespace = parser.parse_args([root, *fixed, *args]) if namespace_update: - namespace_update(namespace, console_context) + namespace_update(namespace) return _capture_output(lambda: _invoke_namespace(namespace)) @@ -424,8 +334,7 @@ def _dispatch_builder_subcommand( module_name: str, builder_name: str, main_handler_name: str, - console_context: ConsoleContext, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> str: parser, subparsers = _parser_root() module = importlib.import_module(module_name) @@ -434,7 +343,7 @@ def _dispatch_builder_subcommand( top_parser.set_defaults(func=getattr(main_module, main_handler_name)) namespace = parser.parse_args([root, *fixed, *args]) if namespace_update: - namespace_update(namespace, console_context) + namespace_update(namespace) return _capture_output(lambda: _invoke_namespace(namespace)) @@ -445,15 +354,14 @@ def _dispatch_adder_subcommand( args: Sequence[str], module_name: str, add_name: str, - console_context: ConsoleContext, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> str: parser, subparsers = _parser_root() module = importlib.import_module(module_name) getattr(module, add_name)(subparsers) namespace = parser.parse_args([root, *fixed, *args]) if namespace_update: - namespace_update(namespace, console_context) + namespace_update(namespace) return _capture_output(lambda: _invoke_namespace(namespace)) @@ -463,7 +371,7 @@ def _extracted_handler( module_name: str, builder_name: str, main_handler_name: str, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> Callable[["HermesConsoleEngine", list[str]], str]: def handler(_engine: HermesConsoleEngine, args: list[str]) -> str: return _dispatch_extracted_subcommand( @@ -473,7 +381,6 @@ def _extracted_handler( module_name=module_name, builder_name=builder_name, main_handler_name=main_handler_name, - console_context=_engine.context, namespace_update=namespace_update, ) @@ -486,7 +393,7 @@ def _registered_handler( module_name: str, register_name: str, handler_name: str | None = None, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> Callable[["HermesConsoleEngine", list[str]], str]: def handler(_engine: HermesConsoleEngine, args: list[str]) -> str: return _dispatch_registered_subcommand( @@ -496,7 +403,6 @@ def _registered_handler( module_name=module_name, register_name=register_name, handler_name=handler_name, - console_context=_engine.context, namespace_update=namespace_update, ) @@ -509,7 +415,7 @@ def _builder_handler( module_name: str, builder_name: str, main_handler_name: str, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> Callable[["HermesConsoleEngine", list[str]], str]: def handler(_engine: HermesConsoleEngine, args: list[str]) -> str: return _dispatch_builder_subcommand( @@ -519,7 +425,6 @@ def _builder_handler( module_name=module_name, builder_name=builder_name, main_handler_name=main_handler_name, - console_context=_engine.context, namespace_update=namespace_update, ) @@ -531,7 +436,7 @@ def _adder_handler( fixed: Sequence[str], module_name: str, add_name: str, - namespace_update: Callable[[argparse.Namespace, ConsoleContext], None] | None = None, + namespace_update: Callable[[argparse.Namespace], None] | None = None, ) -> Callable[["HermesConsoleEngine", list[str]], str]: def handler(_engine: HermesConsoleEngine, args: list[str]) -> str: return _dispatch_adder_subcommand( @@ -540,7 +445,6 @@ def _adder_handler( args=args, module_name=module_name, add_name=add_name, - console_context=_engine.context, namespace_update=namespace_update, ) @@ -554,13 +458,11 @@ def _register_command_family( paths: Iterable[Sequence[str]], handler_factory: Callable[[Sequence[str]], Callable[["HermesConsoleEngine", list[str]], str]], mutating: Iterable[Sequence[str]] = (), - hosted: Iterable[Sequence[str]] = (), summary: str = "", summaries: dict[tuple[str, ...], str] | None = None, confirmation: str = "", ) -> None: mutating_paths = {tuple(path) for path in mutating} - hosted_paths = {tuple(path) for path in hosted} for child_path in paths: child_key = tuple(child_path) full_path = (root, *tuple(child_path)) @@ -573,17 +475,13 @@ def _register_command_family( handler_factory(tuple(child_path)), mutating=child_key in mutating_paths, confirmation=confirmation or f"Run `hermes {usage}`?", - contexts=ALL_CONTEXTS if child_key in hosted_paths else LOCAL_CONTEXTS, ) class HermesConsoleEngine: """Curated line-command executor for Hermes Console.""" - def __init__(self, *, output_limit: int = 20000, context: ConsoleContext = "local"): - if context not in ALL_CONTEXTS: - raise ValueError(f"Unknown console context: {context}") - self.context = context + def __init__(self, *, output_limit: int = 20000): self.output_limit = output_limit self.history: list[str] = [] self.commands: dict[tuple[str, ...], ConsoleCommand] = {} @@ -641,8 +539,6 @@ class HermesConsoleEngine: "Supported commands:", ] for command in sorted(self.commands.values(), key=lambda c: c.usage): - if self.context not in command.contexts: - continue marker = " *" if command.mutating else " " lines.append(f"{marker} {command.usage:<32} {_table_summary(command.summary)}") lines.extend( @@ -655,13 +551,13 @@ class HermesConsoleEngine: return "\n".join(lines) def _register_defaults(self) -> None: - self.register(("status",), "status", "Show Hermes component status.", _status, contexts=ALL_CONTEXTS) - self.register(("doctor",), "doctor", "Run diagnostics without auto-fix.", _doctor, contexts=ALL_CONTEXTS) - self.register(("logs",), "logs [name] [-n N]", "Show recent Hermes logs.", _logs, contexts=ALL_CONTEXTS) - self.register(("sessions", "list"), "sessions list [--limit N]", "List recent sessions.", _sessions_list, contexts=ALL_CONTEXTS) - self.register(("sessions", "stats"), "sessions stats", "Show session store statistics.", _sessions_stats, contexts=ALL_CONTEXTS) - self.register(("config", "show"), "config show", "Show current configuration.", _config_show, contexts=ALL_CONTEXTS) - self.register(("config", "path"), "config path", "Print config.yaml path.", _config_path, contexts=ALL_CONTEXTS) + self.register(("status",), "status", "Show Hermes component status.", _status) + self.register(("doctor",), "doctor", "Run diagnostics without auto-fix.", _doctor) + self.register(("logs",), "logs [name] [-n N]", "Show recent Hermes logs.", _logs) + self.register(("sessions", "list"), "sessions list [--limit N]", "List recent sessions.", _sessions_list) + self.register(("sessions", "stats"), "sessions stats", "Show session store statistics.", _sessions_stats) + self.register(("config", "show"), "config show", "Show current configuration.", _config_show) + self.register(("config", "path"), "config path", "Print config.yaml path.", _config_path) self.register( ("config", "set"), "config set ", @@ -669,10 +565,9 @@ class HermesConsoleEngine: _config_set, mutating=True, confirmation="Update Hermes configuration?", - contexts=ALL_CONTEXTS, ) - self.register(("cron", "list"), "cron list [--all]", "List scheduled jobs.", _cron_list, contexts=ALL_CONTEXTS) - self.register(("cron", "status"), "cron status", "Show cron scheduler status.", _cron_status, contexts=ALL_CONTEXTS) + self.register(("cron", "list"), "cron list [--all]", "List scheduled jobs.", _cron_list) + self.register(("cron", "status"), "cron status", "Show cron scheduler status.", _cron_status) self.register( ("cron", "pause"), "cron pause ", @@ -680,7 +575,6 @@ class HermesConsoleEngine: _cron_pause, mutating=True, confirmation="Pause this cron job?", - contexts=ALL_CONTEXTS, ) self.register( ("cron", "resume"), @@ -689,7 +583,6 @@ class HermesConsoleEngine: _cron_resume, mutating=True, confirmation="Resume this cron job?", - contexts=ALL_CONTEXTS, ) self.register( ("cron", "run"), @@ -698,7 +591,6 @@ class HermesConsoleEngine: _cron_run, mutating=True, confirmation="Trigger this cron job?", - contexts=ALL_CONTEXTS, ) self._register_broad_cli_surface() @@ -1214,8 +1106,6 @@ class HermesConsoleEngine: ), ) - self._mark_hosted(EXPECTED_HOSTED_PATHS) - def register( self, path: Iterable[str], @@ -1225,7 +1115,6 @@ class HermesConsoleEngine: *, mutating: bool = False, confirmation: str = "", - contexts: Iterable[ConsoleContext] = LOCAL_CONTEXTS, ) -> None: key = tuple(path) self.commands[key] = ConsoleCommand( @@ -1235,19 +1124,8 @@ class HermesConsoleEngine: handler=handler, mutating=mutating, confirmation=confirmation, - contexts=frozenset(contexts), ) - def _mark_hosted(self, paths: Iterable[Sequence[str]]) -> None: - for path in paths: - key = tuple(path) - command = self.commands.get(key) - if command is None: - raise RuntimeError(f"Hosted console policy references unknown command: {' '.join(key)}") - self.commands[key] = replace( - command, - contexts=command.contexts | frozenset({"hosted"}), - ) def _execute_builtin(self, tokens: list[str]) -> ConsoleResult | None: head = tokens[0] @@ -1275,29 +1153,14 @@ class HermesConsoleEngine: key = tuple(tokens[:size]) command = self.commands.get(key) if command: - if self.context not in command.contexts: - raise ConsoleCommandError( - f"`hermes {command.usage}` is not available in " - f"{self.context} Hermes Console." - ) - self._enforce_context_policy(command, list(tokens[size:])) return command, list(tokens[size:]) - available = [ - " ".join(path) - for path, command in self.commands.items() - if self.context in command.contexts - ] + available = [" ".join(path) for path in self.commands] probe = " ".join(tokens[:2]) if len(tokens) > 1 else tokens[0] suggestions = difflib.get_close_matches(probe, available, n=3, cutoff=0.45) suffix = f" Did you mean: {', '.join(suggestions)}?" if suggestions else "" raise ConsoleCommandError(f"Unsupported Hermes Console command: {probe}.{suffix}") - def _enforce_context_policy(self, command: ConsoleCommand, args: list[str]) -> None: - if self.context != "hosted": - return - _enforce_hosted_line_policy(command.path, args) - def _rejection_for(self, tokens: Sequence[str]) -> str: first = tokens[0] if first.startswith("-"): @@ -1368,117 +1231,7 @@ def _expect_no_args(args: Sequence[str], usage: str) -> None: raise ConsoleCommandError(f"Usage: {usage}") -HOSTED_CONFIG_ALLOWED_PREFIXES = ( - "display.", - "ui.", - "tts.", - "voice.", - "speech.", - "sessions.", - "cron.", -) -HOSTED_CONFIG_ALLOWED_KEYS = { - "display.interface", -} -HOSTED_CONFIG_BLOCKED_PREFIXES = ( - "auth.", - "dashboard.", - "gateway.", - "managed.", - "model.", - "portal.", - "provider.", - "providers.", - "tool_gateway.", - "custom_providers.", - "mcp_servers.", -) -HOSTED_CONFIG_BLOCKED_NAMES = { - "portal_url", - "portal.url", - "portal.base_url", - "inference_url", - "inference.url", - "inference.base_url", - "nous.portal_url", - "nous.inference_url", - "openrouter_api_key", - "openai_api_key", - "anthropic_api_key", -} - - -def _flag_present(args: Sequence[str], flag: str) -> bool: - return any(arg == flag or arg.startswith(f"{flag}=") for arg in args) - - -def _flag_value(args: Sequence[str], flag: str) -> str | None: - for index, arg in enumerate(args): - if arg == flag: - if index + 1 < len(args): - return args[index + 1] - return "" - prefix = f"{flag}=" - if arg.startswith(prefix): - return arg[len(prefix) :] - return None - - -def _hosted_config_key_allowed(key: str) -> bool: - normalized = key.strip().lower() - if normalized in HOSTED_CONFIG_BLOCKED_NAMES: - return False - if normalized.startswith(HOSTED_CONFIG_BLOCKED_PREFIXES): - return False - return normalized in HOSTED_CONFIG_ALLOWED_KEYS or normalized.startswith( - HOSTED_CONFIG_ALLOWED_PREFIXES - ) - - -def _enforce_hosted_line_policy(path: tuple[str, ...], args: Sequence[str]) -> None: - if path == ("config", "set"): - key = args[0] if args else "" - if key and not _hosted_config_key_allowed(key): - raise ConsoleCommandError( - f"`config set {key}` is not available in hosted Hermes Console. " - "Use the dashboard setting for hosted account/provider changes." - ) - return - - if path == ("mcp", "add"): - if _flag_present(args, "--command") or _flag_present(args, "--args"): - raise ConsoleCommandError( - "Hosted Hermes Console does not add stdio MCP servers. " - "Use catalog install or an HTTP/SSE URL." - ) - if _flag_present(args, "--preset"): - raise ConsoleCommandError( - "Hosted Hermes Console does not add MCP presets directly. " - "Use `mcp install `." - ) - url = _flag_value(args, "--url") - if not url: - raise ConsoleCommandError( - "Hosted Hermes Console requires `mcp add` to use --url with " - "an HTTP/SSE endpoint." - ) - scheme = urlparse(url).scheme.lower() - if scheme not in {"http", "https"}: - raise ConsoleCommandError( - "Hosted Hermes Console only accepts http:// or https:// MCP URLs." - ) - return - - if path in {("cron", "create"), ("cron", "edit")}: - for flag in ("--script", "--no-agent", "--workdir"): - if _flag_present(args, flag): - raise ConsoleCommandError( - f"`cron {' '.join(path[1:])} {flag}` is not available in " - "hosted Hermes Console." - ) - - -def _apply_confirmed_defaults(args: argparse.Namespace, context: ConsoleContext) -> None: +def _apply_confirmed_defaults(args: argparse.Namespace) -> None: """Skip nested prompts after the console-level confirmation has happened.""" for attr in ("yes",): @@ -1764,7 +1517,6 @@ def _profile_status(_engine: HermesConsoleEngine, args: list[str]) -> str: module_name="hermes_cli.subcommands.profile", builder_name="build_profile_parser", main_handler_name="cmd_profile", - console_context=_engine.context, ) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index a50f615d60a..2a5ef1ea937 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -15520,11 +15520,6 @@ def _get_console_executor() -> concurrent.futures.ThreadPoolExecutor: return _console_executor -def _dashboard_console_context() -> str: - """Choose local vs hosted command policy for the dashboard console.""" - return "hosted" if _default_hermes_root_is_opt_data() else "local" - - def _console_profile_from_ws(ws: WebSocket) -> Optional[str]: profile = (ws.query_params.get("profile") or "").strip() return profile or None @@ -15731,16 +15726,12 @@ async def console_ws(ws: WebSocket) -> None: await ws.accept() profile = _console_profile_from_ws(ws) - context = _dashboard_console_context() send_lock = asyncio.Lock() try: from hermes_cli.console_engine import HermesConsoleEngine - engine = HermesConsoleEngine( - output_limit=_CONSOLE_OUTPUT_LIMIT, - context=context, # type: ignore[arg-type] - ) + engine = HermesConsoleEngine(output_limit=_CONSOLE_OUTPUT_LIMIT) if profile and profile.lower() != "current": _resolve_profile_dir(profile) except HTTPException as exc: @@ -15770,11 +15761,10 @@ async def console_ws(ws: WebSocket) -> None: return _log.info( - "console accepted peer=%s mode=%s cred=%s context=%s profile=%s", + "console accepted peer=%s mode=%s cred=%s profile=%s", peer, mode, cred, - context, profile or "current", ) await _console_send( @@ -15782,7 +15772,6 @@ async def console_ws(ws: WebSocket) -> None: send_lock, { "type": "ready", - "context": context, "profile": profile or "current", "prompt": _CONSOLE_PROMPT, }, diff --git a/tests/hermes_cli/test_console_engine.py b/tests/hermes_cli/test_console_engine.py index ac94facbde4..5333efd1095 100644 --- a/tests/hermes_cli/test_console_engine.py +++ b/tests/hermes_cli/test_console_engine.py @@ -339,168 +339,6 @@ def test_console_registry_covers_non_admin_cli_surface(): assert missing == set() -EXPECTED_HOSTED_CONSOLE_COMMANDS = { - ("status",), - ("doctor",), - ("logs",), - ("version",), - ("prompt-size",), - ("insights",), - ("security", "audit"), - ("portal", "info"), - ("portal", "tools"), - ("send",), - ("config", "show"), - ("config", "path"), - ("config", "env-path"), - ("config", "check"), - ("config", "migrate"), - ("config", "set"), - ("sessions", "list"), - ("sessions", "stats"), - ("sessions", "export"), - ("sessions", "rename"), - ("sessions", "optimize"), - ("sessions", "repair"), - ("cron", "list"), - ("cron", "status"), - ("cron", "create"), - ("cron", "edit"), - ("cron", "pause"), - ("cron", "resume"), - ("cron", "run"), - ("cron", "remove"), - ("cron", "tick"), - ("profile",), - ("profile", "list"), - ("profile", "show"), - ("profile", "info"), - ("tools", "list"), - ("tools", "enable"), - ("tools", "disable"), - ("tools", "post-setup"), - ("skills", "browse"), - ("skills", "search"), - ("skills", "inspect"), - ("skills", "list"), - ("skills", "check"), - ("skills", "list-modified"), - ("skills", "diff"), - ("skills", "install"), - ("skills", "update"), - ("skills", "audit"), - ("skills", "uninstall"), - ("skills", "reset"), - ("skills", "opt-in"), - ("skills", "opt-out"), - ("skills", "repair-official"), - ("skills", "snapshot", "export"), - ("skills", "tap", "list"), - ("mcp", "list"), - ("mcp", "catalog"), - ("mcp", "test"), - ("mcp", "add"), - ("mcp", "remove"), - ("mcp", "install"), - ("mcp", "login"), - ("mcp", "reauth"), - ("mcp", "configure"), - ("mcp", "picker"), - ("memory", "status"), - ("auth", "list"), - ("auth", "status"), - ("auth", "reset"), - ("auth", "spotify", "status"), - ("pairing", "list"), - ("pairing", "approve"), - ("pairing", "revoke"), - ("pairing", "clear-pending"), - ("webhook", "list"), - ("webhook", "subscribe"), - ("webhook", "remove"), - ("webhook", "test"), -} - - -def test_hosted_console_registry_exposes_only_hosted_safe_surface(): - engine = HermesConsoleEngine(context="hosted") - hosted = { - path for path, command in engine.commands.items() if "hosted" in command.contexts - } - - assert hosted == EXPECTED_HOSTED_CONSOLE_COMMANDS - - -@pytest.mark.parametrize( - "line", - [ - "portal login", - "auth add nous --type oauth", - "auth logout nous", - "profile create tester", - "profile use default", - "plugins list", - "plugins install owner/repo", - "kanban list", - "hooks list", - "checkpoints clear", - "curator pause", - "pets install cat", - "backup --quick", - "import /tmp/hermes-console-test.zip", - "mcp serve", - "model", - "setup", - "dashboard", - "gateway restart", - "update", - "uninstall", - ], -) -def test_hosted_console_rejects_local_only_or_dangerous_commands(line): - result = HermesConsoleEngine(context="hosted").execute(line) - - assert result.status == "error" - assert result.output - - -@pytest.mark.parametrize( - "line", - [ - "mcp add demo --url https://example.com/sse", - "mcp install n8n", - "mcp configure github", - "mcp picker", - "config set display.interface cli", - "cron create 'every 1h' 'say hello'", - ], -) -def test_hosted_console_allows_guarded_useful_commands_before_confirmation(line): - result = HermesConsoleEngine(context="hosted").execute(line) - - assert result.status == "confirm_required" - - -@pytest.mark.parametrize( - "line", - [ - "mcp add local --command npx --args foo", - "mcp add local --preset unsafe", - "mcp add local --url file:///tmp/server", - "config set model.provider openrouter", - "config set portal.url https://evil.example", - "cron create 'every 1h' 'say hello' --script scripts/ping.py", - "cron create 'every 1h' 'say hello' --no-agent", - "cron edit abc123 --workdir /tmp/project", - ], -) -def test_hosted_console_blocks_known_footgun_arguments_before_confirmation(line): - result = HermesConsoleEngine(context="hosted").execute(line) - - assert result.status == "error" - assert result.output - - @pytest.mark.parametrize( "line", [ diff --git a/tests/hermes_cli/test_web_server_console_ws.py b/tests/hermes_cli/test_web_server_console_ws.py index 538251ec7be..8ff1374ff1d 100644 --- a/tests/hermes_cli/test_web_server_console_ws.py +++ b/tests/hermes_cli/test_web_server_console_ws.py @@ -74,7 +74,6 @@ def test_console_ws_runs_read_only_command(console_client): with console_client.websocket_connect(_url()) as conn: ready = conn.receive_json() assert ready["type"] == "ready" - assert ready["context"] == "local" assert ready["prompt"] == "hermes> " conn.send_json({"type": "input", "line": "help"}) @@ -102,20 +101,6 @@ def test_console_ws_confirmed_command_executes_after_confirmation(console_client assert load_config()["display"]["interface"] == "cli" -def test_console_ws_uses_hosted_context_for_opt_data_policy(console_client, monkeypatch): - monkeypatch.setattr(web_server, "_default_hermes_root_is_opt_data", lambda: True) - - with console_client.websocket_connect(_url()) as conn: - ready = conn.receive_json() - assert ready["type"] == "ready" - assert ready["context"] == "hosted" - - conn.send_json({"type": "input", "line": "profile create nope"}) - - error = _recv_until(conn, "error") - assert "hosted Hermes Console" in error["message"] - - def test_console_ws_cancel_returns_to_prompt(console_client, monkeypatch): from hermes_cli.console_engine import ConsoleResult, HermesConsoleEngine diff --git a/web/src/components/HermesConsoleModal.tsx b/web/src/components/HermesConsoleModal.tsx index fd63b38b8c5..97a2824c79d 100644 --- a/web/src/components/HermesConsoleModal.tsx +++ b/web/src/components/HermesConsoleModal.tsx @@ -17,7 +17,6 @@ import { useTheme } from "@/themes"; type ConsoleFrame = | { type: "ready"; - context?: string; profile?: string; prompt?: string; } @@ -113,7 +112,6 @@ export function HermesConsoleModal({ open, onClose }: HermesConsoleModalProps) { const hasReadyFrameRef = useRef(false); const [connectionState, setConnectionState] = useState("connecting"); - const [consoleContext, setConsoleContext] = useState("pending"); const [consoleProfile, setConsoleProfile] = useState("current"); const { profile } = useProfileScope(); const { theme } = useTheme(); @@ -278,7 +276,6 @@ export function HermesConsoleModal({ open, onClose }: HermesConsoleModalProps) { promptRef.current = nextPrompt; inputPromptRef.current = nextPrompt; hasReadyFrameRef.current = true; - setConsoleContext(frame.context || "local"); setConsoleProfile(frame.profile || "current"); activeCommandRef.current = false; setConnectionState("ready"); @@ -395,7 +392,6 @@ export function HermesConsoleModal({ open, onClose }: HermesConsoleModalProps) { const dataDisposable = term.onData(handleInputData); setConnectionState("connecting"); - setConsoleContext("pending"); setConsoleProfile(profile || "current"); hasReadyFrameRef.current = false; writeLine(term, "\x1b[2mConnecting to Hermes Console...\x1b[0m"); @@ -511,7 +507,6 @@ export function HermesConsoleModal({ open, onClose }: HermesConsoleModalProps) {
{connectionState} - {consoleContext} {consoleProfile}