mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-30 01:32:25 +01:00
fix(security): stop the agent from re-enabling tools an admin disabled (#6357)
* fix(security): stop the agent from re-enabling tools an admin disabled settings.json:disabled_tools is the only global tool-permission boundary the Settings > Agent Tools panel offers, yet manage_settings exposed an `enable_tool` action that removed entries from it. Any agent turn could undo the admin's restriction, and because the agent reads untrusted web pages, emails and documents, a planted instruction could re-enable bash, email or any other tool that had been switched off. The denylist can now only be narrowed from a turn: `disable_tool` and `list_tools` keep working, `enable_tool` is refused with a message that points the user to Settings > Agent Tools, and the action is dropped from the function schema, the prompt rule and the tool index so models are not steered into calling it. The free-form set/delete/reset actions were already unable to reach `disabled_tools`; a test now pins that. Fixes #5523 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UAB5sz5srMKR7rbeDFtdMH * fix(settings): close internal API tool-policy bypass Refuse global tool-policy replacement from internal-token requests even when an admin owner is impersonated, and block the same write in app_api. Route explicit image re-enable requests to list_tools with a Settings pointer. Keep disabled_tools outside general settings actions even if a default is added later. Remove the retired action from capability metadata. Validation: 573 focused tests passed; the running app rejected handler, generic API and encoded-path internal HTTP attempts while preserving human administrator controls. * fix(mcp): block agent writes to per-server MCP tool policy PATCH /api/mcp/servers/{id}/tools only checked require_admin, which accepts the internal tool token, and app_api still forwarded it, so an agent turn could re-enable MCP tools an admin had switched off. Refuse the internal token on that route (same check as POST /api/tools) and add PATCH /api/mcp/servers to the app_api blocklist, with regression tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCbhLNC7mJsunxqfwwshqn --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Afonso Junior <chatgpt@yack.com.br>
This commit is contained in:
co-authored by
Claude Opus 5.5
Afonso Junior
parent
a646b59110
commit
57a1859f08
@@ -585,7 +585,9 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return _ALIASES_SET.get(k2, (k or "").strip())
|
||||
|
||||
def _is_managed_key(key):
|
||||
return key in DEFAULT_SETTINGS and key not in RETIRED_SETTING_KEYS
|
||||
# Tool policy is not a general setting, even if a future default
|
||||
# is added: resetting it must never re-enable disabled tools.
|
||||
return key != "disabled_tools" and key in DEFAULT_SETTINGS and key not in RETIRED_SETTING_KEYS
|
||||
|
||||
_ENUMS = {
|
||||
"image_quality": ["low", "medium", "high"],
|
||||
@@ -713,13 +715,35 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
||||
save_settings(s)
|
||||
return {"response": f"Reset {key} to default ({DEFAULT_SETTINGS[key]}).", "exit_code": 0}
|
||||
|
||||
elif action in ("disable_tool", "enable_tool", "list_tools"):
|
||||
elif action == "enable_tool":
|
||||
# The global disabled_tools denylist is the one hard permission
|
||||
# boundary the Settings -> Agent Tools panel offers admins. It
|
||||
# must only ever be *narrowed* from inside an agent turn: a
|
||||
# model that can call enable_tool can undo the boundary itself,
|
||||
# and any web page, email, or document the agent reads can ask
|
||||
# it to (prompt injection re-enabling bash/email/etc.). Widening
|
||||
# the tool set stays a human action in the Settings panel.
|
||||
tool_name = (args.get("tool") or args.get("name") or "").strip().lower()
|
||||
return {
|
||||
"error": (
|
||||
f"Re-enabling tools is not available from chat"
|
||||
f"{f' (requested: {tool_name})' if tool_name else ''}. "
|
||||
"Disabled tools are an admin policy: ask the user to turn the tool "
|
||||
"back on in Settings > Agent Tools. You can still disable tools "
|
||||
"(disable_tool) or list the current state (list_tools)."
|
||||
),
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
elif action in ("disable_tool", "list_tools"):
|
||||
# Tool-toggle actions. These edit settings.json:disabled_tools
|
||||
# (the global list read on every chat request) rather than
|
||||
# prefs.json. Friendly aliases accepted: "shell" -> "bash",
|
||||
# "search" -> "web_search", "browser" -> browser tools,
|
||||
# "documents" -> the document tool set, "memory" ->
|
||||
# manage_memory, etc.
|
||||
# manage_memory, etc. Only disable_tool mutates: see the
|
||||
# enable_tool branch above for why the agent can never widen
|
||||
# the denylist.
|
||||
from src.settings import get_setting, save_settings, load_settings
|
||||
_ALIASES = {
|
||||
"shell": ["bash"],
|
||||
@@ -753,7 +777,8 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
||||
"response": (
|
||||
f"Currently disabled: {', '.join(current) if current else '(none)'}.\n"
|
||||
"Common toggles: shell (bash), search (web_search), browser, documents, "
|
||||
"memory, skills, images, tasks, notes, calendar, email."
|
||||
"memory, skills, images, tasks, notes, calendar, email. "
|
||||
"To re-enable a tool, open Settings > Agent Tools."
|
||||
),
|
||||
"disabled": list(current),
|
||||
"exit_code": 0,
|
||||
@@ -767,22 +792,19 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
|
||||
settings = load_settings()
|
||||
current = list(settings.get("disabled_tools") or [])
|
||||
before = set(current)
|
||||
if action == "disable_tool":
|
||||
for t in targets:
|
||||
if t not in current:
|
||||
current.append(t)
|
||||
else: # enable_tool
|
||||
current = [t for t in current if t not in targets]
|
||||
for t in targets:
|
||||
if t not in current:
|
||||
current.append(t)
|
||||
after = set(current)
|
||||
settings["disabled_tools"] = current
|
||||
save_settings(settings)
|
||||
|
||||
verb = "Disabled" if action == "disable_tool" else "Enabled"
|
||||
changed = sorted(after.symmetric_difference(before))
|
||||
return {
|
||||
"response": (
|
||||
f"{verb} {tool_name} ({', '.join(targets)}). "
|
||||
f"Now disabled: {', '.join(current) if current else '(none)'}."
|
||||
f"Disabled {tool_name} ({', '.join(targets)}). "
|
||||
f"Now disabled: {', '.join(current) if current else '(none)'}. "
|
||||
"Re-enabling is done by the user in Settings > Agent Tools."
|
||||
),
|
||||
"changed": changed,
|
||||
"disabled": list(current),
|
||||
|
||||
Reference in New Issue
Block a user