diff --git a/src/openjarvis/system.py b/src/openjarvis/system.py index eda3ef5a..a9ade2f8 100644 --- a/src/openjarvis/system.py +++ b/src/openjarvis/system.py @@ -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 diff --git a/src/openjarvis/tools/_stubs.py b/src/openjarvis/tools/_stubs.py index b8a3d4fe..127b4f3b 100644 --- a/src/openjarvis/tools/_stubs.py +++ b/src/openjarvis/tools/_stubs.py @@ -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: diff --git a/tests/security/test_boundary_guard.py b/tests/security/test_boundary_guard.py index 8bfd6dcf..bdcb31ce 100644 --- a/tests/security/test_boundary_guard.py +++ b/tests/security/test_boundary_guard.py @@ -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