mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-11 02:32:20 +02:00
fix(agent): gate tools after external context
Classify built-in tool effects in a server-owned registry and carry run-local external-context integrity state through the agent loop and dispatcher. Block high-impact and unknown actions after successful external results, including same-batch calls, without relying on model compliance.
This commit is contained in:
+40
-3
@@ -33,6 +33,11 @@ from src.settings import get_setting
|
||||
from src.prompt_security import untrusted_context_message
|
||||
from src.tool_security import blocked_tools_for_owner, plan_mode_disabled_tools
|
||||
from src.tool_policy import GUIDE_ONLY_DIRECTIVE, WEB_TOOL_NAMES, ToolPolicy
|
||||
from src.tool_capabilities import (
|
||||
ToolRunSecurityContext,
|
||||
blocked_tool_result,
|
||||
messages_contain_external_untrusted_context,
|
||||
)
|
||||
from src.tool_utils import _truncate, get_mcp_manager
|
||||
from src.agent_tools import (
|
||||
parse_tool_blocks,
|
||||
@@ -3327,6 +3332,7 @@ async def stream_agent_loop(
|
||||
forced_tools: Optional[Set[str]] = None,
|
||||
uploaded_files: Optional[List[Dict]] = None,
|
||||
workload: str = "foreground",
|
||||
external_untrusted_context_seen: bool = False,
|
||||
_is_teacher_run: bool = False,
|
||||
history_session=None,
|
||||
defer_context_shaping: bool = False,
|
||||
@@ -3342,6 +3348,12 @@ async def stream_agent_loop(
|
||||
- data: [DONE] (end)
|
||||
"""
|
||||
|
||||
run_security = ToolRunSecurityContext(
|
||||
external_untrusted_context_seen=(
|
||||
bool(external_untrusted_context_seen)
|
||||
or messages_contain_external_untrusted_context(messages)
|
||||
)
|
||||
)
|
||||
mcp_mgr = get_mcp_manager()
|
||||
prep_timings: Dict[str, float] = {}
|
||||
disabled_tools = set(disabled_tools or [])
|
||||
@@ -4340,6 +4352,17 @@ async def stream_agent_loop(
|
||||
# so the user can resume instead of the turn silently stalling.
|
||||
_exhausted_rounds = False
|
||||
|
||||
def _filter_route_tool_schemas(schemas):
|
||||
if not run_security.external_untrusted_context_seen or not schemas:
|
||||
return schemas
|
||||
return [
|
||||
schema
|
||||
for schema in schemas
|
||||
if run_security.decision_for(
|
||||
(schema.get("function") or {}).get("name") or schema.get("name")
|
||||
).allowed
|
||||
]
|
||||
|
||||
def _tool_schemas_for_route(route_state):
|
||||
route_mcp_schemas = route_state["mcp_schemas"]
|
||||
route_relevant_tools = route_state["relevant_tools"]
|
||||
@@ -4373,10 +4396,11 @@ async def stream_agent_loop(
|
||||
if schema.get("function", {}).get("name") not in disabled_tools
|
||||
and schema.get("name") not in disabled_tools
|
||||
]
|
||||
return schemas
|
||||
return _filter_route_tool_schemas(schemas)
|
||||
|
||||
wants_mcp = any(keyword in _last_user.lower() for keyword in _MCP_KEYWORDS)
|
||||
return route_mcp_schemas if wants_mcp and route_mcp_schemas else []
|
||||
schemas = route_mcp_schemas if wants_mcp and route_mcp_schemas else []
|
||||
return _filter_route_tool_schemas(schemas)
|
||||
|
||||
for round_num in range(1, max_rounds + 1):
|
||||
round_response = ""
|
||||
@@ -5389,11 +5413,21 @@ async def stream_agent_loop(
|
||||
else:
|
||||
cmd_display = full_command
|
||||
|
||||
security_decision = run_security.decision_for(block.tool_type)
|
||||
_ody_clamped_tool_allowed = (
|
||||
_ody_notes_finetune_mode
|
||||
and block.tool_type in {"manage_notes", "manage_calendar", "manage_tasks"}
|
||||
)
|
||||
if tool_policy and tool_policy.blocks(block.tool_type) and not _ody_clamped_tool_allowed:
|
||||
if not security_decision.allowed:
|
||||
desc, result = blocked_tool_result(
|
||||
block.tool_type,
|
||||
security_decision.reason or "Tool blocked by external-context policy.",
|
||||
)
|
||||
logger.info(
|
||||
"Tool blocked before start by external-context policy: %s",
|
||||
block.tool_type,
|
||||
)
|
||||
elif tool_policy and tool_policy.blocks(block.tool_type) and not _ody_clamped_tool_allowed:
|
||||
desc = f"{block.tool_type}: BLOCKED"
|
||||
result = {
|
||||
"error": tool_policy.reason_for(block.tool_type),
|
||||
@@ -5425,6 +5459,7 @@ async def stream_agent_loop(
|
||||
owner=owner,
|
||||
progress_cb=_push_progress,
|
||||
workspace=workspace,
|
||||
security_context=run_security,
|
||||
)
|
||||
finally:
|
||||
# Sentinel so the drainer knows to stop.
|
||||
@@ -5457,6 +5492,8 @@ async def stream_agent_loop(
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
run_security.observe_tool_result(block.tool_type, result)
|
||||
|
||||
# A skill the model just loaded can prescribe tools that weren't
|
||||
# RAG-selected this turn (declared via requires_toolsets in its
|
||||
# frontmatter). Union them into the selection so the NEXT round's
|
||||
|
||||
Reference in New Issue
Block a user