fix: reset ssrf whitelist on config reload and document config refresh

This commit is contained in:
Xubin Ren 2026-04-04 11:35:09 +00:00 committed by Xubin Ren
parent 5f08d61d8f
commit 9ef5b1e145
3 changed files with 49 additions and 3 deletions

View File

@ -856,6 +856,11 @@ Simply send the command above to your nanobot (via CLI or any chat channel), and
Config file: `~/.nanobot/config.json`
> [!NOTE]
> If your config file is older than the current schema, you can refresh it without overwriting your existing values:
> run `nanobot onboard`, then answer `N` when asked whether to overwrite the config.
> nanobot will merge in missing default fields and keep your current settings.
### Providers
> [!TIP]
@ -1235,6 +1240,16 @@ By default, web tools are enabled and web search uses `duckduckgo`, so search wo
If you want to disable all built-in web tools entirely, set `tools.web.enable` to `false`. This removes both `web_search` and `web_fetch` from the tool list sent to the LLM.
If you need to allow trusted private ranges such as Tailscale / CGNAT addresses, you can explicitly exempt them from SSRF blocking with `tools.ssrfWhitelist`:
```json
{
"tools": {
"ssrfWhitelist": ["100.64.0.0/10"]
}
}
```
| Provider | Config fields | Env var fallback | Free |
|----------|--------------|------------------|------|
| `brave` | `apiKey` | `BRAVE_API_KEY` | No |

View File

@ -54,10 +54,9 @@ def load_config(config_path: Path | None = None) -> Config:
def _apply_ssrf_whitelist(config: Config) -> None:
"""Apply SSRF whitelist from config to the network security module."""
if config.tools.ssrf_whitelist:
from nanobot.security.network import configure_ssrf_whitelist
from nanobot.security.network import configure_ssrf_whitelist
configure_ssrf_whitelist(config.tools.ssrf_whitelist)
configure_ssrf_whitelist(config.tools.ssrf_whitelist)
def save_config(config: Config, config_path: Path | None = None) -> None:

View File

@ -1,6 +1,18 @@
import json
import socket
from unittest.mock import patch
from nanobot.config.loader import load_config, save_config
from nanobot.security.network import validate_url_target
def _fake_resolve(host: str, results: list[str]):
"""Return a getaddrinfo mock that maps the given host to fake IP results."""
def _resolver(hostname, port, family=0, type_=0):
if hostname == host:
return [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (ip, 0)) for ip in results]
raise socket.gaierror(f"cannot resolve {hostname}")
return _resolver
def test_load_config_keeps_max_tokens_and_ignores_legacy_memory_window(tmp_path) -> None:
@ -126,3 +138,23 @@ def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch)
assert result.exit_code == 0
saved = json.loads(config_path.read_text(encoding="utf-8"))
assert saved["channels"]["qq"]["msgFormat"] == "plain"
def test_load_config_resets_ssrf_whitelist_when_next_config_is_empty(tmp_path) -> None:
whitelisted = tmp_path / "whitelisted.json"
whitelisted.write_text(
json.dumps({"tools": {"ssrfWhitelist": ["100.64.0.0/10"]}}),
encoding="utf-8",
)
defaulted = tmp_path / "defaulted.json"
defaulted.write_text(json.dumps({}), encoding="utf-8")
load_config(whitelisted)
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("ts.local", ["100.100.1.1"])):
ok, err = validate_url_target("http://ts.local/api")
assert ok, err
load_config(defaulted)
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("ts.local", ["100.100.1.1"])):
ok, _ = validate_url_target("http://ts.local/api")
assert not ok