Harden M7.6/7.7: recursive restriction accumulation, MCP server enforcement, fast-fail authorization
This commit is contained in:
+114
-40
@@ -1,9 +1,11 @@
|
||||
"""Tool execution engine."""
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
@@ -737,7 +739,10 @@ def execute_tool_step(
|
||||
provider_override: Optional[str] = None,
|
||||
dry_run: bool = False,
|
||||
verbose: bool = False,
|
||||
call_stack: Optional[list] = None
|
||||
call_stack: Optional[list] = None,
|
||||
agent_profile: Optional[str] = None,
|
||||
agent_skills: Optional[List[str]] = None,
|
||||
agent_tool_policies: Optional[List[List[str]]] = None,
|
||||
) -> tuple[str, bool]:
|
||||
"""
|
||||
Execute a tool step by calling another tool.
|
||||
@@ -761,7 +766,15 @@ def execute_tool_step(
|
||||
_print_call_stack(call_stack, f"Maximum tool nesting depth ({MAX_TOOL_DEPTH}) exceeded")
|
||||
return "", False
|
||||
|
||||
# Resolve the tool reference
|
||||
# Authorization must happen before resolution because resolution may fetch a
|
||||
# missing tool from the registry.
|
||||
effective_provider = step.provider or provider_override
|
||||
if not _authorize_tool_call(
|
||||
step.tool, effective_provider, agent_tool_policies, call_stack
|
||||
):
|
||||
return "", False
|
||||
|
||||
# Resolve the tool reference only after authorization succeeds.
|
||||
try:
|
||||
resolved = resolve_tool(step.tool)
|
||||
nested_tool = resolved.tool
|
||||
@@ -778,26 +791,6 @@ def execute_tool_step(
|
||||
for key, value in step.args.items():
|
||||
custom_args[key] = substitute_variables(str(value), variables, warn_non_scalar=verbose)
|
||||
|
||||
# Determine effective provider (step override > parent override)
|
||||
effective_provider = step.provider or provider_override
|
||||
|
||||
# Check tool access if the provider restricts which tools it can call
|
||||
if effective_provider:
|
||||
from .providers import get_provider
|
||||
provider_obj = get_provider(effective_provider)
|
||||
if provider_obj and provider_obj.tools is not None:
|
||||
allowed = provider_obj.tools
|
||||
if step.tool not in allowed and not _tool_match_allowlist(step.tool, allowed):
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Provider '{effective_provider}' is not allowed to call tool '{step.tool}'",
|
||||
)
|
||||
print(
|
||||
f"Provider '{effective_provider}' tools allowlist: {allowed}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return "", False
|
||||
|
||||
if verbose:
|
||||
print(f"[verbose] Tool step: calling {step.tool}", file=sys.stderr)
|
||||
print(f"[verbose] Input length: {len(input_text)} chars", file=sys.stderr)
|
||||
@@ -817,8 +810,11 @@ def execute_tool_step(
|
||||
verbose=verbose,
|
||||
_depth=depth + 1,
|
||||
_call_stack=call_stack,
|
||||
agent_profile=step.profile,
|
||||
agent_skills=step.skills,
|
||||
agent_profile=step.profile if step.profile is not None else agent_profile,
|
||||
agent_skills=step.skills if step.skills is not None else agent_skills,
|
||||
agent_tool_policies=_extend_agent_tool_policies(
|
||||
agent_tool_policies, step.tools
|
||||
),
|
||||
)
|
||||
|
||||
return output, exit_code == 0
|
||||
@@ -837,6 +833,7 @@ def run_tool(
|
||||
_call_stack: Optional[list] = None,
|
||||
agent_profile: Optional[str] = None,
|
||||
agent_skills: Optional[List[str]] = None,
|
||||
agent_tool_policies: Optional[List[List[str]]] = None,
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
Execute a tool.
|
||||
@@ -1017,7 +1014,10 @@ def run_tool(
|
||||
provider_override=provider_override,
|
||||
dry_run=dry_run,
|
||||
verbose=verbose,
|
||||
call_stack=step_stack
|
||||
call_stack=step_stack,
|
||||
agent_profile=agent_profile,
|
||||
agent_skills=agent_skills,
|
||||
agent_tool_policies=agent_tool_policies,
|
||||
)
|
||||
if not success:
|
||||
return "", 3
|
||||
@@ -1033,6 +1033,11 @@ def run_tool(
|
||||
if dry_run:
|
||||
variables[step.output_var] = f"[DRY RUN - would call mcp:{step.server}/{step.tool}]"
|
||||
else:
|
||||
step_stack = _call_stack + [(tool.name, i + 1)]
|
||||
if not _authorize_mcp_call(
|
||||
step.server, provider_override, step_stack
|
||||
):
|
||||
return "", 4
|
||||
args = _substitute_mcp_args(step.arguments, variables)
|
||||
try:
|
||||
if mcp_manager is None:
|
||||
@@ -1227,27 +1232,96 @@ def main():
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def _tool_match_allowlist(name: str, allowed: list) -> bool:
|
||||
for pattern in allowed:
|
||||
if pattern == name:
|
||||
return True
|
||||
if pattern.endswith("*") and name.startswith(pattern[:-1]):
|
||||
return True
|
||||
return False
|
||||
def _matches_allowlist(name: str, allowed: List[str]) -> bool:
|
||||
return any(fnmatch.fnmatchcase(name, pattern) for pattern in allowed)
|
||||
|
||||
|
||||
def _authorize_tool_call(
|
||||
name: str,
|
||||
provider_name: Optional[str],
|
||||
agent_tool_policies: Optional[List[List[str]]],
|
||||
call_stack: list,
|
||||
) -> bool:
|
||||
"""Enforce provider and delegated-agent tool policies."""
|
||||
if provider_name:
|
||||
from .providers import get_provider
|
||||
provider = get_provider(provider_name)
|
||||
if provider is None:
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Provider '{provider_name}' is not configured; refusing tool call '{name}'",
|
||||
)
|
||||
return False
|
||||
if provider.tools is not None and not _matches_allowlist(name, provider.tools):
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Provider '{provider_name}' is not allowed to call tool '{name}'",
|
||||
)
|
||||
print(
|
||||
f"Provider '{provider_name}' tools allowlist: {provider.tools}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
for policy in agent_tool_policies or []:
|
||||
if not _matches_allowlist(name, policy):
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Delegated agent is not allowed to call tool '{name}'",
|
||||
)
|
||||
print(f"Delegated agent tools allowlist: {policy}", file=sys.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _extend_agent_tool_policies(
|
||||
policies: Optional[List[List[str]]],
|
||||
additional: Optional[List[str]],
|
||||
) -> Optional[List[List[str]]]:
|
||||
"""Add a restriction without allowing descendants to broaden ancestors."""
|
||||
if additional is None:
|
||||
return policies
|
||||
return [*(policies or []), additional]
|
||||
|
||||
|
||||
def _authorize_mcp_call(
|
||||
server: str,
|
||||
provider_name: Optional[str],
|
||||
call_stack: list,
|
||||
) -> bool:
|
||||
"""Enforce the effective provider's MCP-server policy."""
|
||||
if not provider_name:
|
||||
return True
|
||||
|
||||
from .providers import get_provider
|
||||
provider = get_provider(provider_name)
|
||||
if provider is None:
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Provider '{provider_name}' is not configured; refusing MCP server '{server}'",
|
||||
)
|
||||
return False
|
||||
if provider.mcp_servers is not None and not _matches_allowlist(
|
||||
server, provider.mcp_servers
|
||||
):
|
||||
_print_call_stack(
|
||||
call_stack,
|
||||
f"Provider '{provider_name}' is not allowed to use MCP server '{server}'",
|
||||
)
|
||||
print(
|
||||
f"Provider '{provider_name}' MCP server allowlist: {provider.mcp_servers}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _step_with_profile(step, profile_name):
|
||||
from copy import copy
|
||||
s = copy(step)
|
||||
s.profile = profile_name
|
||||
return s
|
||||
return replace(step, profile=profile_name)
|
||||
|
||||
|
||||
def _step_with_skills(step, skill_names):
|
||||
from copy import copy
|
||||
s = copy(step)
|
||||
s.skills = skill_names
|
||||
return s
|
||||
return replace(step, skills=skill_names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user