refactor(config): nest MyTool settings under tools.my (with legacy-key migration)

This commit is contained in:
Xubin Ren 2026-04-16 15:58:20 +00:00
parent b51da93cbb
commit 90b7d940e8
8 changed files with 100 additions and 10 deletions

View File

@ -18,11 +18,15 @@ Enabled by default (read-only mode). The agent can check its state but not set i
```yaml
tools:
my_enabled: true # default: true
my_set: false # default: false (read-only)
my:
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.

View File

@ -248,8 +248,8 @@ class AgentLoop:
model=self.model,
)
self._register_default_tools()
if _tc.my_enabled:
self.tools.register(MyTool(loop=self, modify_allowed=_tc.my_set))
if _tc.my.enable:
self.tools.register(MyTool(loop=self, modify_allowed=_tc.my.allow_set))
self._runtime_vars: dict[str, Any] = {}
self._current_iteration: int = 0
self.commands = CommandRouter()

View File

@ -284,7 +284,7 @@ class MyTool(Tool):
if action in ("inspect", "check"):
return self._inspect(key)
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"):
return self._modify(key, value)
return f"Unknown action: {action}"

View File

@ -117,4 +117,19 @@ def _migrate_config(data: dict) -> dict:
exec_cfg = tools.get("exec", {})
if "restrictToWorkspace" in exec_cfg and "restrictToWorkspace" not in tools:
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

View File

@ -200,16 +200,22 @@ class MCPServerConfig(Base):
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
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):
"""Tools configuration."""
web: WebToolsConfig = Field(default_factory=WebToolsConfig)
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
my: MyToolConfig = Field(default_factory=MyToolConfig)
restrict_to_workspace: bool = False # restrict all tool access to workspace directory
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)
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):

View File

@ -59,7 +59,7 @@ always: true
- All modifications in-memory only — restart resets everything
- Protected params have type/range validation: `max_iterations` (1100), `context_window_tokens` (40961M), `model` (non-empty str)
- If `my_set` is false, check only
- If `tools.my.allow_set` is false, check only
## Related tools

View File

@ -835,7 +835,7 @@ class TestInspectTaskStatuses:
# ---------------------------------------------------------------------------
# read-only mode (my_set=False)
# read-only mode (tools.my.allow_set=False)
# ---------------------------------------------------------------------------
class TestReadOnlyMode:

View File

@ -140,6 +140,71 @@ def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch)
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:
whitelisted = tmp_path / "whitelisted.json"
whitelisted.write_text(