fix(cli): canonicalize default gateway identity

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent b8333a2d7e
commit a700697583
4 changed files with 70 additions and 10 deletions
+13 -4
View File
@@ -36,6 +36,15 @@ GatewayServiceFactory = Callable[[], Any]
WebUIBundlePreparer = Callable[[Config, BuildMode], None] 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( def create_gateway_app(
*, *,
console: Console, console: Console,
@@ -73,12 +82,12 @@ def create_gateway_app(
def runtime_for_instance(*, workspace: str | None = None, config: str | None = None): def runtime_for_instance(*, workspace: str | None = None, config: str | None = None):
if runtime_factory is not None: if runtime_factory is not None:
return runtime_factory(workspace=workspace, config=config) 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 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( return GatewayRuntime(
paths=GatewayRuntimePaths.for_instance( paths=GatewayRuntimePaths.for_instance(
data_dir=data_dir, data_dir=resolved_config.parent,
workspace=workspace_path, workspace=workspace_path,
config_path=config_path, config_path=config_path,
) )
@@ -101,7 +110,7 @@ def create_gateway_app(
loaded_config: Config | None = None, loaded_config: Config | None = None,
) -> GatewayStartOptions: ) -> GatewayStartOptions:
cfg = loaded_config or load_runtime_config(config, workspace) 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 resolved_workspace = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None
return GatewayStartOptions( return GatewayStartOptions(
port=port if port is not None else cfg.gateway.port, port=port if port is not None else cfg.gateway.port,
+15 -5
View File
@@ -384,16 +384,26 @@ def _print_foreground_port_conflict(
gateway_host: str, gateway_host: str,
gateway_port: int, gateway_port: int,
) -> None: ) -> None:
console.print( gateway_running = _gateway_health_ready(gateway_host, gateway_port)
"[red]Error: nanobot cannot start because one of its local ports is already in use.[/red]" if gateway_running:
) console.print(
console.print(f" WebUI: [cyan]{webui_url}[/cyan]") "[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( console.print(
f" Gateway health: " f" Gateway health: "
f"[cyan]http://{_host_for_local_browser(gateway_host)}:{gateway_port}/health[/cyan]" f"[cyan]http://{_host_for_local_browser(gateway_host)}:{gateway_port}/health[/cyan]"
) )
console.print() 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 status[/cyan]")
console.print(" [cyan]nanobot gateway stop[/cyan]") console.print(" [cyan]nanobot gateway stop[/cyan]")
console.print( console.print(
+30
View File
@@ -2504,6 +2504,10 @@ def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_
_patch_webui_provider_ready(monkeypatch) _patch_webui_provider_ready(monkeypatch)
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None) 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._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._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
monkeypatch.setattr( monkeypatch.setattr(
"nanobot.cli.webui._open_webui_browser", "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) _patch_webui_provider_ready(monkeypatch)
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None) 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._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._webui_endpoint_reachable", lambda *_args, **_kwargs: True)
monkeypatch.setattr("nanobot.cli.webui._tcp_endpoint_reachable", lambda *_args, **_kwargs: False) monkeypatch.setattr("nanobot.cli.webui._tcp_endpoint_reachable", lambda *_args, **_kwargs: False)
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"]) 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 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=<redacted>" in result.stdout
assert "do-not-leak" not in result.stdout
def _patch_serve_runtime(monkeypatch, config: Config, seen: dict[str, object]) -> None: def _patch_serve_runtime(monkeypatch, config: Config, seen: dict[str, object]) -> None:
pytest.importorskip("aiohttp") pytest.importorskip("aiohttp")
+12 -1
View File
@@ -6,7 +6,7 @@ import typer
from rich.console import Console from rich.console import Console
from typer.testing import CliRunner 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.config.schema import Config
from nanobot.gateway import GatewayRuntimePaths, GatewayStartOptions, GatewayStatus, RuntimeResult from nanobot.gateway import GatewayRuntimePaths, GatewayStartOptions, GatewayStatus, RuntimeResult
from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult
@@ -14,6 +14,17 @@ from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult
runner = CliRunner() 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: class FakeRuntime:
def __init__(self, tmp_path: Path): def __init__(self, tmp_path: Path):
self.paths = GatewayRuntimePaths.for_instance(data_dir=tmp_path) self.paths = GatewayRuntimePaths.for_instance(data_dir=tmp_path)