feat(settings): add a tool approval mode with an explicit off (YOLO) option (#6304)

* feat: add optional tool approval mode

Admin-configurable tool_approval_mode setting (ask/auto) for the
post-external-context tool approval gate.

- ask (default): existing approval flow, gate stays on
- auto: bypasses the approval gate for all runs - same effect as
  ODYSSEUS_TOOL_APPROVAL_GATE=0. Documented honestly in the settings
  row copy and in THREAT_MODEL.md, per review on #6304.
- value validated server-side ({ask, auto}, anything else -> 400)
- no new env var: the setting wires to the existing gate control
- tests: setting validation + _tool_approval_gate_bypassed behavior

* feat: surface deployment gate state on Tool approval row

Follow-up to StressTestor's re-review: when ODYSSEUS_TOOL_APPROVAL_GATE is
set to a falsy value, the Tool approval row still read 'Always ask' even though
the gate was off, misleading admins the other way.

GET /api/auth/settings now returns tool_approval_gate_env_disabled (derived
from TOOL_APPROVAL_GATE_ENABLED) for admins, and the settings row renders a
red notice naming the env var and explaining that the gate is off for every
run regardless of the stored setting. Non-admin/scrubbed responses do not
expose the deployment state. Test covers both env values and the scrub path.

* refactor: rename tool approval mode auto to off

The gate-off value is now "off" ("Off (YOLO)" in the UI) so the auto-approve name stays available for the future judged mode.

---------

Co-authored-by: Matej <matej@local>
This commit is contained in:
DracieVajko
2026-11-01 12:22:56 -07:00
committed by GitHub
co-authored by Matej
parent 2a413814c5
commit f7f6abf82b
9 changed files with 168 additions and 6 deletions
+18
View File
@@ -18,6 +18,7 @@ from src.constants import DEEP_RESEARCH_DIR, MEMORY_FILE, PASSWORD_MIN_LENGTH, S
from src.auth_helpers import _auth_disabled
from src.rate_limiter import RateLimiter
from src.settings_scrub import scrub_settings
from src import tool_capabilities
from src.settings import (
load_settings as _load_settings,
save_settings as _save_settings,
@@ -753,6 +754,11 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
user = _get_current_user(request)
settings = without_retired_settings(_load_settings())
if _auth_disabled() or (user and auth_manager.is_admin(user)):
# Deployment-level truth for the Tool approval row: a falsy
# ODYSSEUS_TOOL_APPROVAL_GATE disables the approval gate for every
# run regardless of the stored tool_approval_mode, so surface it to
# admins instead of letting the row mislead them the other way.
settings["tool_approval_gate_env_disabled"] = not tool_capabilities.TOOL_APPROVAL_GATE_ENABLED
return settings
return scrub_settings(settings)
@@ -769,6 +775,9 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
"agent_max_tool_calls": (0, 1000), # 0 = unlimited
"auto_compact_threshold_percent": (50, 95),
}
_VALID_VALUES = {
"tool_approval_mode": {"ask", "off"},
}
for key in DEFAULT_SETTINGS:
if key in RETIRED_SETTING_KEYS:
continue
@@ -782,6 +791,15 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
except (TypeError, ValueError):
raise HTTPException(400, f"{key} must be an integer")
val = max(lo, min(val, hi))
if key in _VALID_VALUES:
allowed = _VALID_VALUES[key]
if not isinstance(val, str) or val not in allowed:
raise HTTPException(
400,
f"{key} must be one of {sorted(allowed)}",
)
current[key] = val
_save_settings(current)
return without_retired_settings(current)