feat: wire BoundaryGuard into ToolExecutor for external tool scanning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-03-28 20:28:33 -07:00
co-authored by Claude Opus 4.6
parent 677ece6fb3
commit d2dbd4c2a2
3 changed files with 84 additions and 0 deletions
+1
View File
@@ -43,6 +43,7 @@ class JarvisSystem:
session_store: Optional[Any] = None # SessionStore
capability_policy: Optional[Any] = None # CapabilityPolicy
audit_logger: Optional[Any] = None # AuditLogger
boundary_guard: Optional[Any] = None # BoundaryGuard
operator_manager: Optional[Any] = None # OperatorManager
agent_manager: Optional[Any] = None # AgentManager
agent_scheduler: Optional[Any] = None # AgentScheduler
+18
View File
@@ -101,6 +101,7 @@ class ToolExecutor:
default_timeout: float = 30.0,
capability_policy: Optional[Any] = None,
agent_id: str = "",
boundary_guard: Optional[Any] = None,
) -> None:
self._tools: Dict[str, BaseTool] = {t.spec.name: t for t in tools}
self._bus = bus
@@ -109,6 +110,7 @@ class ToolExecutor:
self._default_timeout = default_timeout
self._capability_policy = capability_policy
self._agent_id = agent_id
self._boundary_guard = boundary_guard
def execute(self, tool_call: ToolCall) -> ToolResult:
"""Parse arguments, dispatch to tool, measure latency, emit events."""
@@ -130,6 +132,22 @@ class ToolExecutor:
success=False,
)
# Boundary guard: scan external tool arguments
if (
self._boundary_guard is not None
and not getattr(tool, "is_local", True)
):
try:
tool_call = self._boundary_guard.check_outbound(tool_call)
# Re-parse arguments after potential redaction
params = json.loads(tool_call.arguments) if tool_call.arguments else {}
except Exception as exc:
return ToolResult(
tool_name=tool_call.name,
content=f"Security block: {exc}",
success=False,
)
# RBAC capability check
if self._capability_policy and tool.spec.required_capabilities:
for cap in tool.spec.required_capabilities:
+65
View File
@@ -160,3 +160,68 @@ class TestToolTagging:
from openjarvis.tools.calculator import CalculatorTool
assert CalculatorTool.is_local is True
class TestToolExecutorBoundaryIntegration:
"""ToolExecutor should use BoundaryGuard for external tool calls."""
def _make_executor(self, boundary_guard=None):
from openjarvis.tools._stubs import BaseTool, ToolExecutor, ToolSpec
class FakeExternalTool(BaseTool):
tool_id = "fake_external"
is_local = False
@property
def spec(self):
return ToolSpec(
name="fake_external",
description="test",
parameters={
"type": "object",
"properties": {"q": {"type": "string"}},
},
)
def execute(self, **params):
from openjarvis.core.types import ToolResult
return ToolResult(
tool_name="fake_external",
content=f"result for {params.get('q', '')}",
success=True,
)
return ToolExecutor(
tools=[FakeExternalTool()],
boundary_guard=boundary_guard,
)
def test_external_tool_args_scanned(self) -> None:
from openjarvis.core.types import ToolCall
from openjarvis.security.boundary import BoundaryGuard
guard = BoundaryGuard(mode="redact")
executor = self._make_executor(boundary_guard=guard)
tc = ToolCall(
id="t1",
name="fake_external",
arguments=(
'{"q": "my key is sk-proj-abc123def456ghi789jkl012mno345pqr678stu"}'
),
)
result = executor.execute(tc)
assert "sk-proj-" not in result.content
def test_no_guard_passes_through(self) -> None:
from openjarvis.core.types import ToolCall
executor = self._make_executor(boundary_guard=None)
tc = ToolCall(
id="t2",
name="fake_external",
arguments='{"q": "sk-proj-abc123def456ghi789jkl012mno345pqr678stu"}',
)
result = executor.execute(tc)
assert "sk-proj-" in result.content