mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-05 09:15:58 +00:00
refactor(config): nest MyTool settings under tools.my (with legacy-key migration)
This commit is contained in:
parent
b51da93cbb
commit
90b7d940e8
@ -18,11 +18,15 @@ Enabled by default (read-only mode). The agent can check its state but not set i
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
tools:
|
tools:
|
||||||
my_enabled: true # default: true
|
my:
|
||||||
my_set: false # default: false (read-only)
|
enable: true # default: true
|
||||||
|
allow_set: false # default: false (read-only)
|
||||||
```
|
```
|
||||||
|
|
||||||
To allow the agent to set its configuration (e.g. switch models, adjust parameters), set `my_set: true`.
|
To allow the agent to set its configuration (e.g. switch models, adjust parameters), set `tools.my.allow_set: true`.
|
||||||
|
|
||||||
|
Legacy `tools.myEnabled` / `tools.mySet` keys are auto-migrated on load, and
|
||||||
|
rewritten in-place the next time `nanobot onboard` refreshes the config.
|
||||||
|
|
||||||
All modifications are held in memory only — restart restores defaults.
|
All modifications are held in memory only — restart restores defaults.
|
||||||
|
|
||||||
|
|||||||
@ -248,8 +248,8 @@ class AgentLoop:
|
|||||||
model=self.model,
|
model=self.model,
|
||||||
)
|
)
|
||||||
self._register_default_tools()
|
self._register_default_tools()
|
||||||
if _tc.my_enabled:
|
if _tc.my.enable:
|
||||||
self.tools.register(MyTool(loop=self, modify_allowed=_tc.my_set))
|
self.tools.register(MyTool(loop=self, modify_allowed=_tc.my.allow_set))
|
||||||
self._runtime_vars: dict[str, Any] = {}
|
self._runtime_vars: dict[str, Any] = {}
|
||||||
self._current_iteration: int = 0
|
self._current_iteration: int = 0
|
||||||
self.commands = CommandRouter()
|
self.commands = CommandRouter()
|
||||||
|
|||||||
@ -284,7 +284,7 @@ class MyTool(Tool):
|
|||||||
if action in ("inspect", "check"):
|
if action in ("inspect", "check"):
|
||||||
return self._inspect(key)
|
return self._inspect(key)
|
||||||
if not self._modify_allowed:
|
if not self._modify_allowed:
|
||||||
return "Error: set is disabled (my_set is False)"
|
return "Error: set is disabled (tools.my.allow_set is false)"
|
||||||
if action in ("modify", "set"):
|
if action in ("modify", "set"):
|
||||||
return self._modify(key, value)
|
return self._modify(key, value)
|
||||||
return f"Unknown action: {action}"
|
return f"Unknown action: {action}"
|
||||||
|
|||||||
@ -117,4 +117,19 @@ def _migrate_config(data: dict) -> dict:
|
|||||||
exec_cfg = tools.get("exec", {})
|
exec_cfg = tools.get("exec", {})
|
||||||
if "restrictToWorkspace" in exec_cfg and "restrictToWorkspace" not in tools:
|
if "restrictToWorkspace" in exec_cfg and "restrictToWorkspace" not in tools:
|
||||||
tools["restrictToWorkspace"] = exec_cfg.pop("restrictToWorkspace")
|
tools["restrictToWorkspace"] = exec_cfg.pop("restrictToWorkspace")
|
||||||
|
|
||||||
|
# Move tools.myEnabled / tools.mySet → tools.my.{enable, allowSet}.
|
||||||
|
# The old flat keys shipped in the initial MyTool landing; wrapping them in a
|
||||||
|
# sub-config keeps `web` / `exec` / `my` symmetric and gives room to grow.
|
||||||
|
if "myEnabled" in tools or "mySet" in tools:
|
||||||
|
my_cfg = tools.setdefault("my", {})
|
||||||
|
if "myEnabled" in tools and "enable" not in my_cfg:
|
||||||
|
my_cfg["enable"] = tools.pop("myEnabled")
|
||||||
|
else:
|
||||||
|
tools.pop("myEnabled", None)
|
||||||
|
if "mySet" in tools and "allowSet" not in my_cfg:
|
||||||
|
my_cfg["allowSet"] = tools.pop("mySet")
|
||||||
|
else:
|
||||||
|
tools.pop("mySet", None)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@ -200,16 +200,22 @@ class MCPServerConfig(Base):
|
|||||||
tool_timeout: int = 30 # seconds before a tool call is cancelled
|
tool_timeout: int = 30 # seconds before a tool call is cancelled
|
||||||
enabled_tools: list[str] = Field(default_factory=lambda: ["*"]) # Only register these tools; accepts raw MCP names or wrapped mcp_<server>_<tool> names; ["*"] = all tools; [] = no tools
|
enabled_tools: list[str] = Field(default_factory=lambda: ["*"]) # Only register these tools; accepts raw MCP names or wrapped mcp_<server>_<tool> names; ["*"] = all tools; [] = no tools
|
||||||
|
|
||||||
|
class MyToolConfig(Base):
|
||||||
|
"""Self-inspection tool configuration."""
|
||||||
|
|
||||||
|
enable: bool = True # register the `my` tool (agent runtime state inspection)
|
||||||
|
allow_set: bool = False # let `my` modify loop state (read-only if False)
|
||||||
|
|
||||||
|
|
||||||
class ToolsConfig(Base):
|
class ToolsConfig(Base):
|
||||||
"""Tools configuration."""
|
"""Tools configuration."""
|
||||||
|
|
||||||
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
|
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
|
||||||
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
|
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
|
||||||
|
my: MyToolConfig = Field(default_factory=MyToolConfig)
|
||||||
restrict_to_workspace: bool = False # restrict all tool access to workspace directory
|
restrict_to_workspace: bool = False # restrict all tool access to workspace directory
|
||||||
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
||||||
ssrf_whitelist: list[str] = Field(default_factory=list) # CIDR ranges to exempt from SSRF blocking (e.g. ["100.64.0.0/10"] for Tailscale)
|
ssrf_whitelist: list[str] = Field(default_factory=list) # CIDR ranges to exempt from SSRF blocking (e.g. ["100.64.0.0/10"] for Tailscale)
|
||||||
my_enabled: bool = True # enable the my tool (agent runtime state inspection)
|
|
||||||
my_set: bool = False # allow my tool to set state (read-only if False)
|
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseSettings):
|
||||||
|
|||||||
@ -59,7 +59,7 @@ always: true
|
|||||||
|
|
||||||
- All modifications in-memory only — restart resets everything
|
- All modifications in-memory only — restart resets everything
|
||||||
- Protected params have type/range validation: `max_iterations` (1–100), `context_window_tokens` (4096–1M), `model` (non-empty str)
|
- Protected params have type/range validation: `max_iterations` (1–100), `context_window_tokens` (4096–1M), `model` (non-empty str)
|
||||||
- If `my_set` is false, check only
|
- If `tools.my.allow_set` is false, check only
|
||||||
|
|
||||||
## Related tools
|
## Related tools
|
||||||
|
|
||||||
|
|||||||
@ -835,7 +835,7 @@ class TestInspectTaskStatuses:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# read-only mode (my_set=False)
|
# read-only mode (tools.my.allow_set=False)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestReadOnlyMode:
|
class TestReadOnlyMode:
|
||||||
|
|||||||
@ -140,6 +140,71 @@ def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch)
|
|||||||
assert saved["channels"]["qq"]["msgFormat"] == "plain"
|
assert saved["channels"]["qq"]["msgFormat"] == "plain"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_migrates_legacy_my_tool_keys(tmp_path) -> None:
|
||||||
|
config_path = tmp_path / "config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"myEnabled": False,
|
||||||
|
"mySet": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
assert config.tools.my.enable is False
|
||||||
|
assert config.tools.my.allow_set is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_config_rewrites_legacy_my_tool_keys(tmp_path) -> None:
|
||||||
|
config_path = tmp_path / "config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"myEnabled": False,
|
||||||
|
"mySet": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
save_config(config, config_path)
|
||||||
|
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
tools = saved["tools"]
|
||||||
|
assert "myEnabled" not in tools
|
||||||
|
assert "mySet" not in tools
|
||||||
|
assert tools["my"] == {"enable": False, "allowSet": True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_my_tool_keys_take_precedence_over_legacy(tmp_path) -> None:
|
||||||
|
config_path = tmp_path / "config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"myEnabled": False,
|
||||||
|
"mySet": False,
|
||||||
|
"my": {"enable": True, "allowSet": True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
assert config.tools.my.enable is True
|
||||||
|
assert config.tools.my.allow_set is True
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_resets_ssrf_whitelist_when_next_config_is_empty(tmp_path) -> None:
|
def test_load_config_resets_ssrf_whitelist_when_next_config_is_empty(tmp_path) -> None:
|
||||||
whitelisted = tmp_path / "whitelisted.json"
|
whitelisted = tmp_path / "whitelisted.json"
|
||||||
whitelisted.write_text(
|
whitelisted.write_text(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user