Harden M7.6/7.7: recursive restriction accumulation, MCP server enforcement, fast-fail authorization

This commit is contained in:
rob
2026-07-20 02:33:44 -03:00
parent 93ea80950e
commit 8366ef798b
8 changed files with 435 additions and 67 deletions
+179 -2
View File
@@ -1,18 +1,20 @@
"""Tests for runner.py - Tool execution engine."""
import pytest
from types import SimpleNamespace
from unittest.mock import patch, MagicMock
from cmdforge.runner import (
substitute_variables,
execute_prompt_step,
execute_code_step,
execute_tool_step,
run_tool,
create_argument_parser,
collect_custom_args
)
from cmdforge.tool import Tool, ToolArgument, PromptStep, CodeStep
from cmdforge.providers import ProviderResult
from cmdforge.tool import Tool, ToolArgument, PromptStep, CodeStep, ToolStep, McpStep
from cmdforge.providers import Provider, ProviderResult
class TestSubstituteVariables:
@@ -1010,3 +1012,178 @@ class TestCreateArgumentParser:
"max_size": "50",
"format": "json",
}
class TestProviderExecutionPolicies:
def test_denied_tool_is_not_resolved(self, monkeypatch):
resolver = MagicMock()
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolver)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", tools=[]),
)
output, success = execute_tool_step(
ToolStep(tool="forbidden", output_var="out", provider="locked"),
{"input": ""},
)
assert (output, success) == ("", False)
resolver.assert_not_called()
def test_unknown_provider_is_rejected_before_resolution(self, monkeypatch):
resolver = MagicMock()
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolver)
monkeypatch.setattr("cmdforge.providers.get_provider", lambda name: None)
_, success = execute_tool_step(
ToolStep(tool="anything", output_var="out", provider="missing"),
{"input": ""},
)
assert success is False
resolver.assert_not_called()
def test_provider_and_delegated_allowlists_both_apply(self, monkeypatch):
leaf = Tool(name="safe-lint", output="completed")
resolver = MagicMock(return_value=SimpleNamespace(tool=leaf))
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolver)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", tools=["safe-*"]),
)
output, success = execute_tool_step(
ToolStep(tool="safe-lint", output_var="out", provider="restricted"),
{"input": ""},
agent_tool_policies=[["*-lint"]],
)
assert (output, success) == ("completed", True)
resolver.assert_called_once_with("safe-lint")
def test_delegated_allowlist_can_further_restrict_provider(self, monkeypatch):
resolver = MagicMock()
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolver)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", tools=["*"]),
)
_, success = execute_tool_step(
ToolStep(tool="unsafe-delete", output_var="out", provider="broad"),
{"input": ""},
agent_tool_policies=[["safe-*"]],
)
assert success is False
resolver.assert_not_called()
def test_mcp_server_policy_denies_before_manager_creation(self, monkeypatch):
manager = MagicMock()
monkeypatch.setattr("cmdforge.runner.McpClientManager", manager)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", mcp_servers=[]),
)
tool = Tool(
name="mcp-tool",
steps=[McpStep(server="database", tool="query", output_var="result")],
output="{result}",
)
output, exit_code = run_tool(
tool, "", {}, provider_override="restricted"
)
assert (output, exit_code) == ("", 4)
manager.assert_not_called()
def test_mcp_server_policy_allows_matching_server(self, monkeypatch):
class FakeManager:
def call_tool(self, server, tool, arguments, result_mode="auto"):
return "allowed"
monkeypatch.setattr("cmdforge.runner.McpClientManager", FakeManager)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", mcp_servers=["data-*"]),
)
tool = Tool(
name="mcp-tool",
steps=[McpStep(server="data-local", tool="query", output_var="result")],
output="{result}",
)
assert run_tool(tool, "", {}, provider_override="restricted") == (
"allowed", 0
)
def test_agent_context_propagates_through_nested_tools(self, monkeypatch):
captured = {}
leaf = Tool(
name="leaf",
steps=[PromptStep("prompt", "original", "answer")],
output="{answer}",
)
middle = Tool(
name="middle",
steps=[ToolStep(tool="leaf", output_var="child")],
output="{child}",
)
root = Tool(
name="root",
steps=[
ToolStep(
tool="middle",
output_var="delegated",
provider="delegate",
profile="architect",
skills=["python"],
tools=["leaf"],
)
],
output="{delegated}",
)
def resolve(name):
return SimpleNamespace(tool={"middle": middle, "leaf": leaf}[name])
def execute_prompt(step, *args, **kwargs):
captured["step"] = step
captured["provider_override"] = args[1]
return "done", True
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolve)
monkeypatch.setattr("cmdforge.runner.execute_prompt_step", execute_prompt)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", tools=["middle", "leaf"]),
)
assert run_tool(root, "", {}) == ("done", 0)
assert captured["step"].profile == "architect"
assert captured["step"].skills == ["python"]
assert captured["provider_override"] == "delegate"
def test_descendant_cannot_broaden_ancestor_tool_policy(self, monkeypatch):
resolver = MagicMock()
monkeypatch.setattr("cmdforge.runner.resolve_tool", resolver)
monkeypatch.setattr(
"cmdforge.providers.get_provider",
lambda name: Provider(name, "cmd", tools=["*"]),
)
_, success = execute_tool_step(
ToolStep(
tool="dangerous",
output_var="out",
provider="delegate",
tools=["*"],
),
{"input": ""},
agent_tool_policies=[["safe-*"]],
)
assert success is False
resolver.assert_not_called()