mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-02 11:12:21 +01:00
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:
+1
-1
@@ -64,7 +64,7 @@ External content that reaches the LLM is treated as untrusted via `src/prompt_se
|
|||||||
|
|
||||||
`src/tool_capabilities.py` carries a second layer: once untrusted content has entered a run, `ToolRunSecurityContext.decision_for()` blocks tools that execute code, mutate state, or cause external side effects until the user authorises the action separately.
|
`src/tool_capabilities.py` carries a second layer: once untrusted content has entered a run, `ToolRunSecurityContext.decision_for()` blocks tools that execute code, mutate state, or cause external side effects until the user authorises the action separately.
|
||||||
|
|
||||||
It is on unless `ODYSSEUS_TOOL_APPROVAL_GATE` is set to a falsy value (`0`/`false`/`no`/`off`). Request authority narrows which tool families a turn may use, but within an admitted family it does not bind the exact action: a request to read email admits `send_email` and `delete_email`, and agent processes inherit the host network. Until those are covered by their own boundaries, this gate is what stops an injected instruction from reaching them. Turning it off lets a successful injection reach `bash`, `host_shell`, `send_email` and `delete_email` without a separate confirmation.
|
It is on unless `ODYSSEUS_TOOL_APPROVAL_GATE` is set to a falsy value (`0`/`false`/`no`/`off`). The admin-only `tool_approval_mode` setting (Settings → Agent Tools, `off` = "Off (YOLO)") is the same knob exposed in the UI: with `off`, every run sets `approval_gate_bypassed` and this gate stops asking, exactly as if the env var were off. Request authority narrows which tool families a turn may use, but within an admitted family it does not bind the exact action: a request to read email admits `send_email` and `delete_email`, and agent processes inherit the host network. Until those are covered by their own boundaries, this gate is what stops an injected instruction from reaching them. Turning it off — by either knob — lets a successful injection reach `bash`, `host_shell`, `send_email` and `delete_email` without a separate confirmation.
|
||||||
|
|
||||||
Three exemptions apply even when the gate is on, all deliberate:
|
Three exemptions apply even when the gate is on, all deliberate:
|
||||||
|
|
||||||
|
|||||||
@@ -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.auth_helpers import _auth_disabled
|
||||||
from src.rate_limiter import RateLimiter
|
from src.rate_limiter import RateLimiter
|
||||||
from src.settings_scrub import scrub_settings
|
from src.settings_scrub import scrub_settings
|
||||||
|
from src import tool_capabilities
|
||||||
from src.settings import (
|
from src.settings import (
|
||||||
load_settings as _load_settings,
|
load_settings as _load_settings,
|
||||||
save_settings as _save_settings,
|
save_settings as _save_settings,
|
||||||
@@ -753,6 +754,11 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
|||||||
user = _get_current_user(request)
|
user = _get_current_user(request)
|
||||||
settings = without_retired_settings(_load_settings())
|
settings = without_retired_settings(_load_settings())
|
||||||
if _auth_disabled() or (user and auth_manager.is_admin(user)):
|
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 settings
|
||||||
return scrub_settings(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
|
"agent_max_tool_calls": (0, 1000), # 0 = unlimited
|
||||||
"auto_compact_threshold_percent": (50, 95),
|
"auto_compact_threshold_percent": (50, 95),
|
||||||
}
|
}
|
||||||
|
_VALID_VALUES = {
|
||||||
|
"tool_approval_mode": {"ask", "off"},
|
||||||
|
}
|
||||||
for key in DEFAULT_SETTINGS:
|
for key in DEFAULT_SETTINGS:
|
||||||
if key in RETIRED_SETTING_KEYS:
|
if key in RETIRED_SETTING_KEYS:
|
||||||
continue
|
continue
|
||||||
@@ -782,6 +791,15 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise HTTPException(400, f"{key} must be an integer")
|
raise HTTPException(400, f"{key} must be an integer")
|
||||||
val = max(lo, min(val, hi))
|
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
|
current[key] = val
|
||||||
_save_settings(current)
|
_save_settings(current)
|
||||||
return without_retired_settings(current)
|
return without_retired_settings(current)
|
||||||
|
|||||||
+16
-4
@@ -14,12 +14,12 @@ import csv
|
|||||||
import difflib
|
import difflib
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from src.web_recovery import WebRecoveryBudget
|
from src.web_recovery import WebRecoveryBudget
|
||||||
from itertools import count
|
from itertools import count
|
||||||
@@ -16994,6 +16994,20 @@ def _detect_runaway_call(call_freq, threshold=15):
|
|||||||
sig = next((s for s, n in call_freq.items() if n >= threshold), None)
|
sig = next((s for s, n in call_freq.items() if n >= threshold), None)
|
||||||
return sig.split(":", 1)[0] if sig else None
|
return sig.split(":", 1)[0] if sig else None
|
||||||
|
|
||||||
|
def _tool_approval_gate_bypassed(
|
||||||
|
exact_approval: Optional[ExactToolApproval] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Return whether the post-external-context tool approval gate is bypassed.
|
||||||
|
|
||||||
|
``tool_approval_mode=off`` (admin setting, Settings → Agent Tools) bypasses
|
||||||
|
the gate on every run, the same effect as ``ODYSSEUS_TOOL_APPROVAL_GATE=0``.
|
||||||
|
A chat-session grant bypasses it only within the scope of its own task.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
get_setting("tool_approval_mode", "ask") == "off"
|
||||||
|
or bool(exact_approval and exact_approval.allow_remaining_actions)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _tool_result_signature(tool_result_records: list[dict]) -> str:
|
def _tool_result_signature(tool_result_records: list[dict]) -> str:
|
||||||
"""Return a bounded fingerprint of the observable result of a tool batch.
|
"""Return a bounded fingerprint of the observable result of a tool batch.
|
||||||
@@ -21437,9 +21451,7 @@ async def stream_agent_loop(
|
|||||||
)
|
)
|
||||||
or messages_contain_external_untrusted_context(messages)
|
or messages_contain_external_untrusted_context(messages)
|
||||||
),
|
),
|
||||||
approval_gate_bypassed=bool(
|
approval_gate_bypassed=_tool_approval_gate_bypassed(exact_approval),
|
||||||
exact_approval and exact_approval.allow_remaining_actions
|
|
||||||
),
|
|
||||||
delegated_credential=bool(delegated_credential),
|
delegated_credential=bool(delegated_credential),
|
||||||
)
|
)
|
||||||
_has_tui_host_bridge = _tui_host_bridge_is_usable(client_runtime_context)
|
_has_tui_host_bridge = _tui_host_bridge_is_usable(client_runtime_context)
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ DEFAULT_SETTINGS = {
|
|||||||
# Tune via Settings or by editing data/settings.json.
|
# Tune via Settings or by editing data/settings.json.
|
||||||
"research_run_timeout_seconds": 1800,
|
"research_run_timeout_seconds": 1800,
|
||||||
"agent_max_tool_calls": 0,
|
"agent_max_tool_calls": 0,
|
||||||
|
"tool_approval_mode": "ask",
|
||||||
"agent_max_rounds": 20, # per-message agent step cap (clamped 1..200)
|
"agent_max_rounds": 20, # per-message agent step cap (clamped 1..200)
|
||||||
# Soft input-token budget for the agent loop. The DEFAULT value (6000) is the
|
# Soft input-token budget for the agent loop. The DEFAULT value (6000) is the
|
||||||
# "auto" sentinel: it means "scale the budget to the model's context window"
|
# "auto" sentinel: it means "scale the budget to the model's context window"
|
||||||
|
|||||||
@@ -2463,6 +2463,22 @@
|
|||||||
<label class="settings-label">Max steps per message</label>
|
<label class="settings-label">Max steps per message</label>
|
||||||
<input id="set-agentMaxRounds" type="text" inputmode="numeric" placeholder="20" class="settings-select" style="width:120px;">
|
<input id="set-agentMaxRounds" type="text" inputmode="numeric" placeholder="20" class="settings-select" style="width:120px;">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label class="settings-label">Tool approval</label>
|
||||||
|
<select id="set-toolApprovalMode" class="settings-select">
|
||||||
|
<option value="ask">Always ask</option>
|
||||||
|
<option value="off">Off (YOLO)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-toggle-sub" style="margin-top:4px;">
|
||||||
|
Auto-approve turns off the tool approval gate (same control as the
|
||||||
|
ODYSSEUS_TOOL_APPROVAL_GATE env var). Bash, host_shell, send_email and
|
||||||
|
delete_email then run without a confirmation even after the agent has
|
||||||
|
seen untrusted external content (email, web pages, documents), so a
|
||||||
|
prompt-injected instruction can reach them directly.
|
||||||
|
</div>
|
||||||
|
<div id="set-toolApprovalEnvNote" class="admin-toggle-sub" style="margin-top:4px;display:none;color:var(--red);"></div>
|
||||||
<div id="set-agentMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
<div id="set-agentMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export async function initAgentSettings() {
|
|||||||
var toolsInput = el('set-agentMaxTools');
|
var toolsInput = el('set-agentMaxTools');
|
||||||
var roundsInput = el('set-agentMaxRounds');
|
var roundsInput = el('set-agentMaxRounds');
|
||||||
var supInput = el('set-agentSupervisorLadder');
|
var supInput = el('set-agentSupervisorLadder');
|
||||||
|
var approvalModeInput = el('set-toolApprovalMode');
|
||||||
var msg = el('set-agentMsg');
|
var msg = el('set-agentMsg');
|
||||||
if (!toolsInput) return;
|
if (!toolsInput) return;
|
||||||
|
|
||||||
@@ -19,6 +20,21 @@ export async function initAgentSettings() {
|
|||||||
if (settings.agent_max_tool_calls) toolsInput.value = settings.agent_max_tool_calls;
|
if (settings.agent_max_tool_calls) toolsInput.value = settings.agent_max_tool_calls;
|
||||||
if (roundsInput && settings.agent_max_rounds) roundsInput.value = settings.agent_max_rounds;
|
if (roundsInput && settings.agent_max_rounds) roundsInput.value = settings.agent_max_rounds;
|
||||||
if (supInput) supInput.checked = !!settings.agent_supervisor_ladder;
|
if (supInput) supInput.checked = !!settings.agent_supervisor_ladder;
|
||||||
|
if (approvalModeInput) {
|
||||||
|
approvalModeInput.value =
|
||||||
|
settings.tool_approval_mode === 'off' ? 'off' : 'ask';
|
||||||
|
}
|
||||||
|
var approvalEnvNote = el('set-toolApprovalEnvNote');
|
||||||
|
if (approvalEnvNote) {
|
||||||
|
if (settings.tool_approval_gate_env_disabled) {
|
||||||
|
approvalEnvNote.textContent =
|
||||||
|
'Forced by deployment: ODYSSEUS_TOOL_APPROVAL_GATE is set to a falsy value on this server, so the approval gate is off for every run regardless of this setting.';
|
||||||
|
approvalEnvNote.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
approvalEnvNote.textContent = '';
|
||||||
|
approvalEnvNote.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// Clamp + coerce a raw input to an int in [lo, hi]; falls back to `dflt`
|
// Clamp + coerce a raw input to an int in [lo, hi]; falls back to `dflt`
|
||||||
@@ -37,6 +53,10 @@ export async function initAgentSettings() {
|
|||||||
var payload = { agent_max_tool_calls: tools };
|
var payload = { agent_max_tool_calls: tools };
|
||||||
if (rounds != null) payload.agent_max_rounds = rounds;
|
if (rounds != null) payload.agent_max_rounds = rounds;
|
||||||
if (supInput) payload.agent_supervisor_ladder = !!supInput.checked;
|
if (supInput) payload.agent_supervisor_ladder = !!supInput.checked;
|
||||||
|
if (approvalModeInput) {
|
||||||
|
payload.tool_approval_mode =
|
||||||
|
approvalModeInput.value === 'off' ? 'off' : 'ask';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await _postSettings(payload);
|
await _postSettings(payload);
|
||||||
msg.textContent = (tools > 0 ? 'Limit: ' + tools + ' tool calls' : 'Unlimited tool calls') +
|
msg.textContent = (tools > 0 ? 'Limit: ' + tools + ' tool calls' : 'Unlimited tool calls') +
|
||||||
@@ -49,6 +69,7 @@ export async function initAgentSettings() {
|
|||||||
toolsInput.addEventListener('change', save);
|
toolsInput.addEventListener('change', save);
|
||||||
if (roundsInput) roundsInput.addEventListener('change', save);
|
if (roundsInput) roundsInput.addEventListener('change', save);
|
||||||
if (supInput) supInput.addEventListener('change', save);
|
if (supInput) supInput.addEventListener('change', save);
|
||||||
|
if (approvalModeInput) approvalModeInput.addEventListener('change', save);
|
||||||
var cur = parseInt(toolsInput.value, 10) || 0;
|
var cur = parseInt(toolsInput.value, 10) || 0;
|
||||||
var curR = roundsInput ? (parseInt(roundsInput.value, 10) || 20) : null;
|
var curR = roundsInput ? (parseInt(roundsInput.value, 10) || 20) : null;
|
||||||
msg.textContent = (cur > 0 ? 'Limit: ' + cur + ' tool calls' : 'Unlimited tool calls') +
|
msg.textContent = (cur > 0 ? 'Limit: ' + cur + ' tool calls' : 'Unlimited tool calls') +
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -117,3 +118,62 @@ def test_manage_settings_tombstones_legacy_fallback_key(monkeypatch):
|
|||||||
|
|
||||||
assert save_calls == []
|
assert save_calls == []
|
||||||
assert store["default_model_fallbacks"] == LEGACY_VALUE
|
assert store["default_model_fallbacks"] == LEGACY_VALUE
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_approval_mode_accepts_only_supported_values(monkeypatch):
|
||||||
|
store = dict(settings_mod.DEFAULT_SETTINGS)
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth_routes, "migrate_from_settings", lambda: None)
|
||||||
|
monkeypatch.setattr(auth_routes, "_load_settings", lambda: dict(store))
|
||||||
|
|
||||||
|
def save_settings(updated):
|
||||||
|
store.clear()
|
||||||
|
store.update(updated)
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth_routes, "_save_settings", save_settings)
|
||||||
|
|
||||||
|
router = auth_routes.setup_auth_routes(_AuthManager())
|
||||||
|
set_settings = _route(router, "/api/auth/settings", "POST")
|
||||||
|
|
||||||
|
response = await set_settings(
|
||||||
|
_Request({"tool_approval_mode": "off"}, admin=True)
|
||||||
|
)
|
||||||
|
assert response["tool_approval_mode"] == "off"
|
||||||
|
assert store["tool_approval_mode"] == "off"
|
||||||
|
|
||||||
|
response = await set_settings(
|
||||||
|
_Request({"tool_approval_mode": "ask"}, admin=True)
|
||||||
|
)
|
||||||
|
assert response["tool_approval_mode"] == "ask"
|
||||||
|
assert store["tool_approval_mode"] == "ask"
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await set_settings(
|
||||||
|
_Request({"tool_approval_mode": "banana"}, admin=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 400
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_settings_expose_gate_env_kill_switch_to_admins_only(monkeypatch):
|
||||||
|
store = dict(settings_mod.DEFAULT_SETTINGS)
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth_routes, "migrate_from_settings", lambda: None)
|
||||||
|
monkeypatch.setattr(auth_routes, "_load_settings", lambda: dict(store))
|
||||||
|
|
||||||
|
router = auth_routes.setup_auth_routes(_AuthManager())
|
||||||
|
get_settings = _route(router, "/api/auth/settings", "GET")
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth_routes.tool_capabilities, "TOOL_APPROVAL_GATE_ENABLED", True)
|
||||||
|
response = await get_settings(_Request(admin=True))
|
||||||
|
assert response["tool_approval_gate_env_disabled"] is False
|
||||||
|
|
||||||
|
monkeypatch.setattr(auth_routes.tool_capabilities, "TOOL_APPROVAL_GATE_ENABLED", False)
|
||||||
|
response = await get_settings(_Request(admin=True))
|
||||||
|
assert response["tool_approval_gate_env_disabled"] is True
|
||||||
|
# The stored setting itself is untouched by the env kill-switch.
|
||||||
|
assert response["tool_approval_mode"] == "ask"
|
||||||
|
|
||||||
|
# Unauthenticated/scrubbed callers must not learn the deployment gate state.
|
||||||
|
response = await get_settings(_Request())
|
||||||
|
assert "tool_approval_gate_env_disabled" not in response
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ def test_route_context_agent_frontend_and_cache_bust_wire_the_contract():
|
|||||||
assert "_without_latest_matching_user_message(" not in helpers
|
assert "_without_latest_matching_user_message(" not in helpers
|
||||||
assert "selected_tools=approval_selected_tools" in agent
|
assert "selected_tools=approval_selected_tools" in agent
|
||||||
assert "continuation_query=_retrieval_query or _last_user" in agent
|
assert "continuation_query=_retrieval_query or _last_user" in agent
|
||||||
assert "approval_gate_bypassed=bool(" in agent
|
assert "approval_gate_bypassed=_tool_approval_gate_bypassed(" in agent
|
||||||
assert "['approve', 'approve_task', 'deny']" in frontend
|
assert "['approve', 'approve_task', 'deny']" in frontend
|
||||||
assert "input.value = label" not in frontend
|
assert "input.value = label" not in frontend
|
||||||
assert "const msg = approvalForSend ? '' : el('message').value;" in frontend
|
assert "const msg = approvalForSend ? '' : el('message').value;" in frontend
|
||||||
|
|||||||
@@ -500,3 +500,37 @@ async def test_dispatcher_revalidates_sealed_workspace(monkeypatch, tmp_path):
|
|||||||
|
|
||||||
assert result["blocked"] is True
|
assert result["blocked"] is True
|
||||||
assert result["policy"] == "exact_tool_approval"
|
assert result["policy"] == "exact_tool_approval"
|
||||||
|
|
||||||
|
def test_tool_approval_mode_defaults_to_ask(monkeypatch):
|
||||||
|
import src.agent_loop as agent_loop
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
agent_loop,
|
||||||
|
"get_setting",
|
||||||
|
lambda key, default=None: (
|
||||||
|
"ask" if key == "tool_approval_mode" else default
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert agent_loop._tool_approval_gate_bypassed() is False
|
||||||
|
|
||||||
|
def test_tool_approval_gate_respects_setting(monkeypatch):
|
||||||
|
import src.agent_loop as agent_loop
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
agent_loop,
|
||||||
|
"get_setting",
|
||||||
|
lambda key, default=None: (
|
||||||
|
"ask" if key == "tool_approval_mode" else default
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert agent_loop._tool_approval_gate_bypassed() is False
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
agent_loop,
|
||||||
|
"get_setting",
|
||||||
|
lambda key, default=None: (
|
||||||
|
"off" if key == "tool_approval_mode" else default
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert agent_loop._tool_approval_gate_bypassed() is True
|
||||||
|
|||||||
Reference in New Issue
Block a user