mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(webui): repair bootstrap secret launch paths
This commit is contained in:
parent
88143a8bf0
commit
444f488563
@ -951,19 +951,20 @@ def _webui_display_url(url: str) -> str:
|
|||||||
return f"{prefix}{marker}<redacted>"
|
return f"{prefix}{marker}<redacted>"
|
||||||
|
|
||||||
|
|
||||||
def _ensure_local_webui_channel(config: Config, *, port: int | None, yes: bool) -> bool:
|
def _ensure_local_webui_channel(config: Config, *, port: int | None, yes: bool) -> tuple[bool, bool]:
|
||||||
"""Enable the local WebUI channel with safe localhost defaults."""
|
"""Enable the local WebUI channel with safe localhost defaults."""
|
||||||
from nanobot.channels.websocket import WebSocketConfig
|
from nanobot.channels.websocket import WebSocketConfig
|
||||||
|
|
||||||
current = getattr(config.channels, "websocket", None) or {}
|
current = getattr(config.channels, "websocket", None) or {}
|
||||||
model = WebSocketConfig.model_validate(current)
|
model = WebSocketConfig.model_validate(current)
|
||||||
changed = False
|
changed = False
|
||||||
|
generated_secret = False
|
||||||
|
|
||||||
needs_enable = not model.enabled
|
needs_enable = not model.enabled
|
||||||
needs_port = port is not None and model.port != port
|
needs_port = port is not None and model.port != port
|
||||||
needs_secret = not model.token_issue_secret.strip() and not model.token.strip()
|
needs_secret = not model.token_issue_secret.strip() and not model.token.strip()
|
||||||
if not needs_enable and not needs_port and not needs_secret:
|
if not needs_enable and not needs_port and not needs_secret:
|
||||||
return False
|
return False, False
|
||||||
|
|
||||||
target_port = port if port is not None else model.port
|
target_port = port if port is not None else model.port
|
||||||
console.print()
|
console.print()
|
||||||
@ -993,9 +994,10 @@ def _ensure_local_webui_channel(config: Config, *, port: int | None, yes: bool)
|
|||||||
|
|
||||||
model.token_issue_secret = secrets.token_urlsafe(32)
|
model.token_issue_secret = secrets.token_urlsafe(32)
|
||||||
changed = True
|
changed = True
|
||||||
|
generated_secret = True
|
||||||
|
|
||||||
setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True))
|
setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True))
|
||||||
return changed
|
return changed, generated_secret
|
||||||
|
|
||||||
|
|
||||||
def _warn_webui_bind_scope(config: Config) -> None:
|
def _warn_webui_bind_scope(config: Config) -> None:
|
||||||
@ -1256,7 +1258,11 @@ def webui(
|
|||||||
setup_config.agents.defaults.workspace = workspace
|
setup_config.agents.defaults.workspace = workspace
|
||||||
|
|
||||||
try:
|
try:
|
||||||
changed_webui = _ensure_local_webui_channel(setup_config, port=port, yes=yes)
|
changed_webui, generated_bootstrap_secret = _ensure_local_webui_channel(
|
||||||
|
setup_config,
|
||||||
|
port=port,
|
||||||
|
yes=yes,
|
||||||
|
)
|
||||||
_warn_webui_bind_scope(setup_config)
|
_warn_webui_bind_scope(setup_config)
|
||||||
webui_url = _webui_browser_url(setup_config)
|
webui_url = _webui_browser_url(setup_config)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@ -1279,6 +1285,14 @@ def webui(
|
|||||||
console.print(f"Gateway health: [cyan]http://{runtime_config.gateway.host}:{effective_gateway_port}/health[/cyan]")
|
console.print(f"Gateway health: [cyan]http://{runtime_config.gateway.host}:{effective_gateway_port}/health[/cyan]")
|
||||||
if no_open:
|
if no_open:
|
||||||
console.print("[dim]Browser opening disabled by --no-open.[/dim]")
|
console.print("[dim]Browser opening disabled by --no-open.[/dim]")
|
||||||
|
if generated_bootstrap_secret:
|
||||||
|
console.print(
|
||||||
|
"[yellow]A WebUI bootstrap secret was generated and saved in this config.[/yellow]"
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
"[dim]Open the WebUI and enter channels.websocket.tokenIssueSecret from "
|
||||||
|
f"{config_path}, or rerun without --no-open to open the authenticated URL.[/dim]"
|
||||||
|
)
|
||||||
|
|
||||||
if background:
|
if background:
|
||||||
config_arg = str(config_path)
|
config_arg = str(config_path)
|
||||||
@ -1290,18 +1304,27 @@ def webui(
|
|||||||
config_path=config_arg,
|
config_path=config_arg,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = runtime.start_background(
|
start_options = GatewayStartOptions(
|
||||||
GatewayStartOptions(
|
port=effective_gateway_port,
|
||||||
port=effective_gateway_port,
|
workspace=workspace_arg,
|
||||||
workspace=workspace_arg,
|
config_path=config_arg,
|
||||||
config_path=config_arg,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if not result.ok and result.message != "gateway_already_running":
|
result = runtime.start_background(start_options)
|
||||||
console.print(f"[yellow]Gateway was not started: {result.message}[/yellow]")
|
restarted = False
|
||||||
|
restart_attempted = False
|
||||||
|
if not result.ok and result.message == "gateway_already_running" and changed_webui:
|
||||||
|
restart_attempted = True
|
||||||
|
console.print("[yellow]WebUI config changed; restarting the background gateway.[/yellow]")
|
||||||
|
result = runtime.restart(start_options, timeout_s=20)
|
||||||
|
restarted = result.ok
|
||||||
|
if not result.ok and (restart_attempted or result.message != "gateway_already_running"):
|
||||||
|
action = "restarted" if restart_attempted else "started"
|
||||||
|
console.print(f"[yellow]Gateway was not {action}: {result.message}[/yellow]")
|
||||||
console.print(f"Logs: {result.status.log_path}")
|
console.print(f"Logs: {result.status.log_path}")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
if result.ok:
|
if restarted:
|
||||||
|
console.print("[green]Gateway restarted in the background.[/green]")
|
||||||
|
elif result.ok:
|
||||||
console.print("[green]Gateway started in the background.[/green]")
|
console.print("[green]Gateway started in the background.[/green]")
|
||||||
else:
|
else:
|
||||||
console.print("[yellow]Gateway is already running in the background.[/yellow]")
|
console.print("[yellow]Gateway is already running in the background.[/yellow]")
|
||||||
|
|||||||
@ -1672,6 +1672,10 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
|
|||||||
assert data["agents"]["defaults"]["workspace"] == str(workspace)
|
assert data["agents"]["defaults"]["workspace"] == str(workspace)
|
||||||
assert seen["templates"] == workspace
|
assert seen["templates"] == workspace
|
||||||
assert seen["gateway_kwargs"] == {"port": 18888, "open_browser_url": None}
|
assert seen["gateway_kwargs"] == {"port": 18888, "open_browser_url": None}
|
||||||
|
compact_output = _strip_ansi(result.stdout).replace("\n", " ")
|
||||||
|
assert "bootstrap secret was generated" in compact_output
|
||||||
|
assert "channels.websocket.tokenIssueSecret" in compact_output
|
||||||
|
assert "rerun without --no-open" in compact_output
|
||||||
|
|
||||||
|
|
||||||
def test_webui_yes_refuses_missing_provider_setup(monkeypatch, tmp_path: Path) -> None:
|
def test_webui_yes_refuses_missing_provider_setup(monkeypatch, tmp_path: Path) -> None:
|
||||||
@ -1753,6 +1757,80 @@ def test_webui_background_starts_runtime_and_opens_browser(monkeypatch, tmp_path
|
|||||||
assert "bootstrapSecret=" in opened_url
|
assert "bootstrapSecret=" in opened_url
|
||||||
|
|
||||||
|
|
||||||
|
def test_webui_background_restarts_when_config_changes_and_gateway_is_running(
|
||||||
|
monkeypatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
from nanobot.gateway import GatewayStartOptions, GatewayStatus, RuntimeResult
|
||||||
|
|
||||||
|
config_file = tmp_path / "config.json"
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
config_file.write_text("{}")
|
||||||
|
seen: dict[str, object] = {}
|
||||||
|
_patch_webui_provider_ready(monkeypatch)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
|
||||||
|
def _status(options: GatewayStartOptions) -> GatewayStatus:
|
||||||
|
return GatewayStatus(
|
||||||
|
running=True,
|
||||||
|
pid=123,
|
||||||
|
state_path=tmp_path / "gateway.json",
|
||||||
|
log_path=tmp_path / "gateway.log",
|
||||||
|
port=options.port,
|
||||||
|
reason="running",
|
||||||
|
)
|
||||||
|
|
||||||
|
class _FakeRuntime:
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
seen["runtime_kwargs"] = kwargs
|
||||||
|
|
||||||
|
def start_background(self, options: GatewayStartOptions) -> RuntimeResult:
|
||||||
|
seen["start_options"] = options
|
||||||
|
return RuntimeResult(False, "gateway_already_running", _status(options))
|
||||||
|
|
||||||
|
def restart(self, options: GatewayStartOptions, *, timeout_s: int) -> RuntimeResult:
|
||||||
|
seen["restart_options"] = options
|
||||||
|
seen["restart_timeout"] = timeout_s
|
||||||
|
return RuntimeResult(True, "gateway_started_background", _status(options))
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.cli.commands._open_webui_browser",
|
||||||
|
lambda url: seen.__setitem__("opened_url", url),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
"webui",
|
||||||
|
"--config",
|
||||||
|
str(config_file),
|
||||||
|
"--workspace",
|
||||||
|
str(workspace),
|
||||||
|
"--background",
|
||||||
|
"--gateway-port",
|
||||||
|
"18889",
|
||||||
|
"--yes",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
compact_output = _strip_ansi(result.stdout).replace("\n", " ")
|
||||||
|
assert "WebUI config changed; restarting the background gateway" in compact_output
|
||||||
|
assert "Gateway restarted in the background" in compact_output
|
||||||
|
assert "Gateway is already running" not in compact_output
|
||||||
|
options = seen["restart_options"]
|
||||||
|
assert isinstance(options, GatewayStartOptions)
|
||||||
|
assert options is seen["start_options"]
|
||||||
|
assert seen["restart_timeout"] == 20
|
||||||
|
assert options.port == 18889
|
||||||
|
assert options.config_path == str(config_file.resolve(strict=False))
|
||||||
|
assert options.workspace == str(workspace.resolve(strict=False))
|
||||||
|
opened_url = seen["opened_url"]
|
||||||
|
assert isinstance(opened_url, str)
|
||||||
|
assert opened_url.startswith("http://127.0.0.1:8765/#/?bootstrapSecret=")
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user