diff --git a/nanobot/cli/gateway.py b/nanobot/cli/gateway.py index 87d87cb99..33ac9b133 100644 --- a/nanobot/cli/gateway.py +++ b/nanobot/cli/gateway.py @@ -36,6 +36,15 @@ GatewayServiceFactory = Callable[[], Any] WebUIBundlePreparer = Callable[[Config, BuildMode], None] +def _resolved_config_selector(config: str | None) -> Path: + """Return the one canonical config identity used by every local client.""" + if config: + return Path(config).expanduser().resolve(strict=False) + from nanobot.config.loader import get_config_path + + return get_config_path().resolve(strict=False) + + def create_gateway_app( *, console: Console, @@ -73,12 +82,12 @@ def create_gateway_app( def runtime_for_instance(*, workspace: str | None = None, config: str | None = None): if runtime_factory is not None: return runtime_factory(workspace=workspace, config=config) - config_path = str(Path(config).expanduser().resolve(strict=False)) if config else None + resolved_config = _resolved_config_selector(config) + config_path = str(resolved_config) workspace_path = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None - data_dir = Path(config_path).parent if config_path else None return GatewayRuntime( paths=GatewayRuntimePaths.for_instance( - data_dir=data_dir, + data_dir=resolved_config.parent, workspace=workspace_path, config_path=config_path, ) @@ -101,7 +110,7 @@ def create_gateway_app( loaded_config: Config | None = None, ) -> GatewayStartOptions: cfg = loaded_config or load_runtime_config(config, workspace) - resolved_config = str(Path(config).expanduser().resolve()) if config else None + resolved_config = str(_resolved_config_selector(config)) if config else None resolved_workspace = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None return GatewayStartOptions( port=port if port is not None else cfg.gateway.port, diff --git a/nanobot/cli/webui_support.py b/nanobot/cli/webui_support.py index 3423c13ba..331791bb7 100644 --- a/nanobot/cli/webui_support.py +++ b/nanobot/cli/webui_support.py @@ -384,16 +384,26 @@ def _print_foreground_port_conflict( gateway_host: str, gateway_port: int, ) -> None: - console.print( - "[red]Error: nanobot cannot start because one of its local ports is already in use.[/red]" - ) - console.print(f" WebUI: [cyan]{webui_url}[/cyan]") + gateway_running = _gateway_health_ready(gateway_host, gateway_port) + if gateway_running: + console.print( + "[yellow]A nanobot gateway is already running for this local instance.[/yellow]" + ) + else: + console.print( + "[red]Error: nanobot cannot start because one of its local ports " + "is already in use.[/red]" + ) + console.print(f" WebUI: [cyan]{_webui_display_url(webui_url)}[/cyan]") console.print( f" Gateway health: " f"[cyan]http://{_host_for_local_browser(gateway_host)}:{gateway_port}/health[/cyan]" ) console.print() - console.print("If this is an existing nanobot instance, use it or stop it first:") + if gateway_running: + console.print("Use the existing instance, or stop it first:") + else: + console.print("If this is an existing nanobot instance, use it or stop it first:") console.print(" [cyan]nanobot gateway status[/cyan]") console.print(" [cyan]nanobot gateway stop[/cyan]") console.print( diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index 05ba3eac4..514424a3d 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -2504,6 +2504,10 @@ def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_ _patch_webui_provider_ready(monkeypatch) monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None) monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: True) + monkeypatch.setattr( + "nanobot.cli.webui_support._gateway_health_ready", + lambda *_args, **_kwargs: True, + ) monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: True) monkeypatch.setattr( "nanobot.cli.webui._open_webui_browser", @@ -2628,6 +2632,10 @@ def test_webui_foreground_refuses_occupied_webui_port(monkeypatch, tmp_path: Pat _patch_webui_provider_ready(monkeypatch) monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None) monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: False) + monkeypatch.setattr( + "nanobot.cli.webui_support._gateway_health_ready", + lambda *_args, **_kwargs: False, + ) monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: True) monkeypatch.setattr("nanobot.cli.webui._tcp_endpoint_reachable", lambda *_args, **_kwargs: False) result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"]) @@ -2638,6 +2646,28 @@ def test_webui_foreground_refuses_occupied_webui_port(monkeypatch, tmp_path: Pat assert "--gateway-port" in result.stdout +def test_webui_foreground_reports_an_existing_gateway_without_leaking_secret( + monkeypatch, + tmp_path: Path, +) -> None: + config_file = tmp_path / "config.json" + config_file.write_text( + '{"channels":{"websocket":{"tokenIssueSecret":"do-not-leak"}}}', + encoding="utf-8", + ) + _patch_webui_provider_ready(monkeypatch) + monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None) + monkeypatch.setattr("nanobot.cli.webui._gateway_health_ready", lambda *_args, **_kwargs: True) + monkeypatch.setattr("nanobot.cli.webui._webui_endpoint_reachable", lambda *_args, **_kwargs: False) + + result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"]) + + assert result.exit_code == 1 + assert "gateway is already running for this local instance" in result.stdout + assert "bootstrapSecret=" in result.stdout + assert "do-not-leak" not in result.stdout + + def _patch_serve_runtime(monkeypatch, config: Config, seen: dict[str, object]) -> None: pytest.importorskip("aiohttp") diff --git a/tests/cli/test_gateway_commands.py b/tests/cli/test_gateway_commands.py index e40a5bf81..1a2e213c3 100644 --- a/tests/cli/test_gateway_commands.py +++ b/tests/cli/test_gateway_commands.py @@ -6,7 +6,7 @@ import typer from rich.console import Console from typer.testing import CliRunner -from nanobot.cli.gateway import create_gateway_app +from nanobot.cli.gateway import _resolved_config_selector, create_gateway_app from nanobot.config.schema import Config from nanobot.gateway import GatewayRuntimePaths, GatewayStartOptions, GatewayStatus, RuntimeResult from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult @@ -14,6 +14,17 @@ from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult runner = CliRunner() +def test_default_config_has_the_same_gateway_identity_when_explicit( + monkeypatch, + tmp_path: Path, +) -> None: + config_path = tmp_path / "config.json" + monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path) + + assert _resolved_config_selector(None) == config_path + assert _resolved_config_selector(str(config_path)) == config_path + + class FakeRuntime: def __init__(self, tmp_path: Path): self.paths = GatewayRuntimePaths.for_instance(data_dir=tmp_path)