fix: secure quick start webui setup

Add Quick Start endpoint choices for subscription plan providers, require explicit WebSocket confirmation, and require a WebUI password when enabling the WebSocket channel. Update docs to route Quick Start users through the WebUI instead of agent -m.
This commit is contained in:
chengyongru
2026-06-22 13:04:05 +08:00
committed by Xubin Ren
parent e5294002ed
commit fca1f2ad02
9 changed files with 361 additions and 35 deletions
+12 -2
View File
@@ -234,7 +234,7 @@ Windows PowerShell:
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
```
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps below and go straight to **Test one message**.
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, skip the manual initialize/configure steps below and go straight to **Open the WebUI**.
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
@@ -353,7 +353,17 @@ For another provider, the same config shape still applies:
| Model ID | `modelPresets.primary.model` |
| Endpoint URL, only when needed | `providers.<provider>.apiBase` |
**3. Test one message**
**3. Open the WebUI**
If Quick Start enabled the WebSocket channel, start the gateway:
```bash
nanobot gateway
```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there.
For manual or terminal-only setup, test one CLI message:
```bash
nanobot status
+2 -2
View File
@@ -201,7 +201,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client
> - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers.
> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
> - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config.
> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.com/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`.
> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.ai/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`.
> - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config.
> - **Xiaomi MiMo thinking mode**: MiMo models (e.g. `mimo-v2.5-pro`) default to enabled thinking. Use `agents.defaults.reasoningEffort: "none"` to disable it, or `"low"` / `"medium"` / `"high"` to keep it on. Omitting the field preserves the provider's per-model default.
> - **Xiaomi MiMo Token Plan**: If you're on MiMo's token plan, set `"apiBase": "https://token-plan-sgp.xiaomimimo.com/v1"` in your xiaomi_mimo provider config.
@@ -752,7 +752,7 @@ Step Plan is StepFun's subscription-based service for high-frequency AI develope
"providers": {
"stepfun": {
"apiKey": "${STEPFUN_API_KEY}",
"apiBase": "https://api.stepfun.com/step_plan/v1"
"apiBase": "https://api.stepfun.ai/step_plan/v1"
}
},
"modelPresets": {
+2 -2
View File
@@ -272,7 +272,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i
"providers": {
"stepfun": {
"apiKey": "${STEPFUN_API_KEY}",
"apiBase": "https://api.stepfun.com/step_plan/v1"
"apiBase": "https://api.stepfun.ai/step_plan/v1"
}
},
"tools": {
@@ -285,7 +285,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i
}
```
`apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.com/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider.
`apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.ai/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider.
### Zhipu
+14 -2
View File
@@ -32,7 +32,7 @@ On Windows PowerShell:
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
```
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps and go straight to [Check the Setup](#4-check-the-setup).
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, go straight to [Open the WebUI](#5-open-the-webui).
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
@@ -233,7 +233,19 @@ Read it like this:
| `Model` | The model or preset you expect. |
| Provider list | Most providers can say `not set`; the provider used by the active preset should show a check mark, OAuth status, or local URL. |
## 5. Test One Message
## 5. Open the WebUI
If Quick Start enabled the WebSocket channel, start the gateway:
```bash
nanobot gateway
```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there.
## 6. Test One CLI Message
Use this path if you skipped Quick Start, declined the WebSocket channel, or want a terminal-only check.
Run a one-shot CLI message:
+14 -8
View File
@@ -167,6 +167,7 @@ Move through the wizard like this:
|---|---|
| A menu | Use the arrow keys to highlight an option, then press `Enter`. |
| The provider menu | Choose the company or service you want to use. |
| An endpoint menu | Choose the standard API or subscription plan endpoint that matches your key. |
| An API key field | Paste the key, then press `Enter`. |
| A provider base URL field | Paste the provider base URL from its docs, then press `Enter`. |
| The Model ID field | Paste a model name from your provider, then press `Enter`. |
@@ -176,12 +177,15 @@ For the first setup, choose `[Q] Quick Start`. It configures the recommended loc
1. Choose `[Q] Quick Start`.
2. Choose the provider you want to use.
3. Paste your API key if the wizard asks for one.
4. Paste the provider base URL if the wizard asks for one.
5. Paste a model ID that provider can run.
6. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
3. Choose the endpoint if the wizard asks, such as Standard API, Coding Plan, Token Plan, or Step Plan.
4. Paste your API key if the wizard asks for one.
5. Paste the provider base URL if the wizard asks for one.
6. Paste a model ID that provider can run.
7. Confirm that Quick Start should enable the WebSocket channel for the local WebUI.
8. Set the WebUI password when prompted.
9. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
The recommended path enables the local WebUI and default AI settings. You do not need to choose a chat channel for the first run.
The recommended path enables `channels.websocket` for the local WebUI, requires a WebUI password, and writes default AI settings. You do not need to choose a separate chat app for the first run.
If you already know that you need custom headers, provider-specific request fields, a chat app, or tools, choose `Advanced Settings` instead. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`.
@@ -192,7 +196,7 @@ The wizard creates or updates:
| `~/.nanobot/config.json` | Settings file. |
| `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. |
If Quick Start finished successfully, skip to [Send the First Message](#7-send-the-first-message). The next two sections are only for manual setup.
If Quick Start finished successfully, skip to [Open the WebUI](#7-open-the-webui). The next two sections are only for manual setup.
## Manual Setup: How to Merge JSON Snippets
@@ -294,7 +298,7 @@ For copyable provider-specific examples, use [`provider-cookbook.md`](./provider
Save the file.
## 7. Send the First Message
## 7. Open the WebUI
First check that nanobot can read the saved setup:
@@ -312,7 +316,9 @@ Start the local browser UI:
nanobot gateway
```
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser and send:
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. If Quick Start enabled the WebSocket channel, enter the WebUI password you set in the wizard.
Send this first message in the browser:
```text
Hello!
+2 -1
View File
@@ -26,7 +26,8 @@ Add to `config.json` under `channels.websocket`:
"host": "127.0.0.1",
"port": 8765,
"path": "/",
"websocketRequiresToken": false,
"tokenIssueSecret": "your-webui-password",
"websocketRequiresToken": true,
"allowFrom": ["*"],
"streaming": true
}
+12 -2
View File
@@ -15,10 +15,19 @@ First confirm your provider and model can answer:
nanobot agent -m "Hello!"
```
Then merge the WebSocket channel into your existing `~/.nanobot/config.json`:
Then merge the WebSocket channel into your existing `~/.nanobot/config.json`.
Set `tokenIssueSecret` to the password you will enter in the WebUI login form:
```json
{ "channels": { "websocket": { "enabled": true } } }
{
"channels": {
"websocket": {
"enabled": true,
"tokenIssueSecret": "your-webui-password",
"websocketRequiresToken": true
}
}
}
```
If you are new to JSON snippets, see
@@ -34,6 +43,7 @@ Leave the gateway running and open
[`http://127.0.0.1:8765`](http://127.0.0.1:8765). The WebUI is served by the
WebSocket channel on port `8765` by default. The gateway health endpoint,
`18790` by default, is not the browser UI.
Enter `tokenIssueSecret` when the WebUI asks for a password.
## What It Is For
+123 -16
View File
@@ -46,6 +46,13 @@ class _QuickStartProviderInfo(NamedTuple):
is_direct: bool
class _QuickStartEndpointChoice(NamedTuple):
"""Provider endpoint option used by Quick Start."""
label: str
api_base: str
# --- Field Hints for Select Fields ---
# Maps field names to (choices, hint_text)
# To add a new select field with hints, add an entry:
@@ -69,7 +76,32 @@ _QUICK_START_CUSTOM_PROVIDER_CHOICE = "Other OpenAI-compatible"
_CLEAR_CHOICE = "Clear value"
_QUICK_START_MENU_CHOICE = "[Q] Quick Start"
_QUICK_START_STEPS = ("Provider + model", "WebUI", "Review")
_QUICK_START_STEPS = ("Provider setup", "WebSocket channel", "Review")
_QUICK_START_ENDPOINT_CHOICES: dict[str, tuple[_QuickStartEndpointChoice, ...]] = {
"zhipu": (
_QuickStartEndpointChoice("Standard API", "https://open.bigmodel.cn/api/paas/v4"),
_QuickStartEndpointChoice("Coding Plan", "https://open.bigmodel.cn/api/coding/paas/v4"),
),
"minimax": (
_QuickStartEndpointChoice("Global API", "https://api.minimax.io/v1"),
_QuickStartEndpointChoice("Mainland China Token Plan", "https://api.minimaxi.com/v1"),
),
"minimax_anthropic": (
_QuickStartEndpointChoice("Global Anthropic API", "https://api.minimax.io/anthropic"),
_QuickStartEndpointChoice(
"Mainland China Anthropic Token Plan",
"https://api.minimaxi.com/anthropic",
),
),
"stepfun": (
_QuickStartEndpointChoice("Standard API", "https://api.stepfun.com/v1"),
_QuickStartEndpointChoice("Step Plan", "https://api.stepfun.ai/step_plan/v1"),
),
"xiaomi_mimo": (
_QuickStartEndpointChoice("Standard API", "https://api.xiaomimimo.com/v1"),
_QuickStartEndpointChoice("Token Plan", "https://token-plan-sgp.xiaomimimo.com/v1"),
),
}
# Low-contrast terminal palette inspired by JetBrains Darcula/Islands.
_UI_ACCENT = "#6B9BFA"
@@ -487,6 +519,17 @@ def _input_text(display_name: str, current: Any, field_type: str, field_info=Non
return value
def _input_secret(display_name: str) -> str | None:
"""Get a secret value without echoing it when questionary supports password input."""
prompt_factory = getattr(_get_questionary(), "password", None)
if prompt_factory is None:
prompt_factory = _get_questionary().text
value = prompt_factory(f"{display_name}:").ask()
if value is None:
return None
return str(value).strip()
def _input_with_existing(
display_name: str, current: Any, field_type: str, field_info=None
) -> Any:
@@ -1467,6 +1510,8 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider
"""Return whether Quick Start must ask for a provider base URL."""
if provider_name == "custom":
return True
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
return False
if info is None or info.default_api_base:
return False
return info.backend == "azure_openai" or (
@@ -1474,6 +1519,43 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider
)
def _select_quick_start_api_base(
provider_name: str,
provider_display: str,
info: _QuickStartProviderInfo | None,
) -> tuple[str, bool] | None:
"""Return the api_base and whether the user explicitly selected or entered it."""
endpoint_choices = _QUICK_START_ENDPOINT_CHOICES.get(provider_name)
if endpoint_choices:
choices = {choice.label: choice.api_base for choice in endpoint_choices}
answer = _select_with_back(
f"Which {provider_display} endpoint should Quick Start use?",
list(choices) + ["<- Back"],
default=endpoint_choices[0].label,
)
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
return None
assert isinstance(answer, str)
return choices[answer], True
api_base = info.default_api_base if info else ""
if not _quick_start_requires_base_url(provider_name, info):
return api_base, False
base_answer = _input_text(
"Provider base URL",
api_base,
"str",
)
if base_answer is None:
return None
api_base = base_answer.strip().rstrip("/")
if not api_base:
console.print("[yellow]! Provider base URL is required for this provider[/yellow]")
return None
return api_base, True
def _configure_quick_start_provider(config: Config) -> bool:
"""Configure the beginner path from provider credentials and model."""
_show_quick_start_progress(1)
@@ -1489,6 +1571,14 @@ def _configure_quick_start_provider(config: Config) -> bool:
provider_name = provider_choices[answer]
provider_info = _get_quick_start_provider_info().get(provider_name)
api_base = provider_info.default_api_base if provider_info else ""
base_was_prompted = False
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
if api_base_result is None:
return False
api_base, base_was_prompted = api_base_result
api_key: str | None = None
if _quick_start_requires_api_key(provider_name, provider_info):
api_key = _input_text(f"{answer} API key", "", "str")
@@ -1499,21 +1589,14 @@ def _configure_quick_start_provider(config: Config) -> bool:
console.print("[yellow]! API key is required for Quick Start[/yellow]")
return False
api_base = provider_info.default_api_base if provider_info else ""
base_was_prompted = False
if _quick_start_requires_base_url(provider_name, provider_info):
base_answer = _input_text(
"Provider base URL",
api_base,
"str",
)
if base_answer is None:
return False
base_was_prompted = True
api_base = base_answer.strip().rstrip("/")
if not api_base:
console.print("[yellow]! Provider base URL is required for this provider[/yellow]")
if (
provider_name not in _QUICK_START_ENDPOINT_CHOICES
and _quick_start_requires_base_url(provider_name, provider_info)
):
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
if api_base_result is None:
return False
api_base, base_was_prompted = api_base_result
provider_config = getattr(config.providers, provider_name, None)
if provider_config is None:
@@ -1545,6 +1628,25 @@ def _configure_quick_start_provider(config: Config) -> bool:
def _enable_quick_start_websocket_defaults(config: Config) -> bool:
"""Enable local WebUI with the default WebSocket settings."""
_show_quick_start_progress(2)
console.print(
f"[{_UI_ACCENT}]Quick Start will enable the WebSocket channel for the local WebUI.[/]"
)
console.print(
f"[{_UI_MUTED}]This lets the browser UI at http://127.0.0.1:8765 connect to nanobot.[/]"
)
console.print()
answer = _get_questionary().confirm(
"Enable WebSocket channel now?",
default=True,
).ask()
if not answer:
console.print("[yellow]! Quick Start needs the WebSocket channel for the local WebUI[/yellow]")
return False
webui_secret = _input_secret("Set a WebUI password")
if not webui_secret:
console.print("[yellow]! WebUI password is required when enabling WebSocket[/yellow]")
return False
config_cls = _get_channel_config_class("websocket")
if config_cls is None:
console.print("[red]No configuration class found for websocket[/red]")
@@ -1554,6 +1656,10 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
model = config_cls.model_validate(current)
if hasattr(model, "enabled"):
setattr(model, "enabled", True)
if hasattr(model, "token_issue_secret"):
setattr(model, "token_issue_secret", webui_secret)
if hasattr(model, "websocket_requires_token"):
setattr(model, "websocket_requires_token", True)
setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True))
return True
@@ -1581,6 +1687,7 @@ def _show_quick_start_summary(config: Config) -> None:
rows = [
("Status", status),
("Next", next_step),
("WebSocket channel", "enabled"),
("Open", "http://127.0.0.1:8765"),
]
_print_summary_panel(rows, "Quick Start")
@@ -1591,7 +1698,7 @@ def _configure_quick_start(config: Config) -> bool:
console.clear()
_show_section_header(
"Quick Start",
"Choose the API provider, paste the key, enter the model, then use the local WebUI.",
"Choose provider endpoint, add credentials and model, then enable the local WebUI channel.",
)
if not _configure_quick_start_provider(config):
_pause()
+180
View File
@@ -942,6 +942,13 @@ class TestMainMenuUpdate:
pause_messages: list[str] = []
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek")
@@ -951,6 +958,14 @@ class TestMainMenuUpdate:
"_input_model_with_autocomplete",
lambda *a, **kw: "deepseek-v4-flash",
)
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt("webui-secret"),
),
)
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
@@ -966,6 +981,7 @@ class TestMainMenuUpdate:
websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True
assert websocket["tokenIssueSecret"] == "webui-secret"
def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch):
"""Known providers should ask users for the model instead of fetching one."""
@@ -1034,6 +1050,90 @@ class TestMainMenuUpdate:
assert config.model_presets["primary"].provider == "openai"
assert config.model_presets["primary"].model == "gpt-4o-mini"
def test_quick_start_zhipu_coding_plan_uses_coding_base_url(self, monkeypatch):
"""Zhipu Coding Plan should not use the standard Zhipu base URL."""
config = Config()
choices = iter(["Zhipu AI", "Coding Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "zhipu-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "glm-4.6",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.zhipu.api_key == "zhipu-key"
assert config.providers.zhipu.api_base == "https://open.bigmodel.cn/api/coding/paas/v4"
assert config.model_presets["primary"].provider == "zhipu"
assert config.model_presets["primary"].model == "glm-4.6"
def test_quick_start_minimax_mainland_token_plan_uses_mainland_base_url(self, monkeypatch):
"""MiniMax mainland token plan should not use the global MiniMax base URL."""
config = Config()
choices = iter(["MiniMax", "Mainland China Token Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "minimax-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "MiniMax-M2",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.minimax.api_key == "minimax-key"
assert config.providers.minimax.api_base == "https://api.minimaxi.com/v1"
assert config.model_presets["primary"].provider == "minimax"
assert config.model_presets["primary"].model == "MiniMax-M2"
def test_quick_start_stepfun_step_plan_uses_plan_base_url(self, monkeypatch):
"""StepFun Step Plan should not use the standard StepFun base URL."""
config = Config()
choices = iter(["Step Fun", "Step Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "stepfun-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "step-3.5-flash",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.stepfun.api_key == "stepfun-key"
assert config.providers.stepfun.api_base == "https://api.stepfun.ai/step_plan/v1"
assert config.model_presets["primary"].provider == "stepfun"
assert config.model_presets["primary"].model == "step-3.5-flash"
def test_quick_start_xiaomi_mimo_token_plan_uses_token_plan_base_url(self, monkeypatch):
"""Xiaomi MiMo Token Plan should not use the standard MiMo base URL."""
config = Config()
choices = iter(["Xiaomi MIMO", "Token Plan"])
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices))
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "mimo-key")
monkeypatch.setattr(
onboard_wizard,
"_input_model_with_autocomplete",
lambda *a, **kw: "mimo-v2.5-pro",
)
assert onboard_wizard._configure_quick_start_provider(config) is True
assert config.providers.xiaomi_mimo.api_key == "mimo-key"
assert config.providers.xiaomi_mimo.api_base == "https://token-plan-sgp.xiaomimimo.com/v1"
assert config.model_presets["primary"].provider == "xiaomi_mimo"
assert config.model_presets["primary"].model == "mimo-v2.5-pro"
def test_quick_start_custom_base_url_asks_for_model_id(self, monkeypatch):
"""Custom providers should ask for base URL and model ID."""
config = Config()
@@ -1080,6 +1180,85 @@ class TestMainMenuUpdate:
assert config.model_presets["primary"].provider == "azure_openai"
assert config.model_presets["primary"].model == "deployment-name"
def test_quick_start_websocket_step_explains_channel_enablement(self, monkeypatch):
"""Quick Start should confirm and protect WebSocket for WebUI."""
config = Config()
messages: list[str] = []
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda message="", *a, **kw: messages.append(str(message)))
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt("webui-secret"),
),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is True
assert any("WebSocket channel" in message for message in messages)
assert any("http://127.0.0.1:8765" in message for message in messages)
websocket = getattr(config.channels, "websocket")
assert websocket["enabled"] is True
assert websocket["websocketRequiresToken"] is True
assert websocket["tokenIssueSecret"] == "webui-secret"
def test_quick_start_websocket_step_can_be_declined(self, monkeypatch):
"""Declining WebSocket should stop Quick Start before changing channel config."""
config = Config()
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(confirm=lambda *a, **kw: FakePrompt(False)),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False
assert getattr(config.channels, "websocket", None) is None
def test_quick_start_websocket_requires_password(self, monkeypatch):
"""Accepting WebSocket with an empty password should not enable the channel."""
config = Config()
class FakePrompt:
def __init__(self, response):
self.response = response
def ask(self):
return self.response
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard,
"questionary",
SimpleNamespace(
confirm=lambda *a, **kw: FakePrompt(True),
password=lambda *a, **kw: FakePrompt(""),
),
)
assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False
assert getattr(config.channels, "websocket", None) is None
def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch):
"""Quick Start should not create a ready-looking config without an API key."""
config = Config()
@@ -1135,6 +1314,7 @@ class TestMainMenuUpdate:
assert rows["Status"] == "DeepSeek API key missing"
assert "API key" in rows["Next"]
assert "nanobot gateway" in rows["Next"]
assert "agent -m" not in rows["Next"]
assert labels.index("Next") < labels.index("Open")
assert "Model" not in rows
assert "Entry point" not in rows