diff --git a/python/packages/hyperlight/README.md b/python/packages/hyperlight/README.md index e35c8de0f20..3c700c4b32e 100644 --- a/python/packages/hyperlight/README.md +++ b/python/packages/hyperlight/README.md @@ -2,6 +2,23 @@ Hyperlight-backed CodeAct integrations for Microsoft Agent Framework. +## FIDES compatibility + +> [!WARNING] +> **FIDES integration is not supported with CodeAct providers.** Host-tool calls +> made inside generated code bypass the agent's function middleware, so FIDES +> policy enforcement and label tracking do not apply to those nested calls or +> intermediate code values. Checks on the outer `execute_code` invocation do not +> enforce the policies of individual tools called inside it. +> +> Registering tools with recognized FIDES `additional_properties` logs a warning, +> but does not block execution. This is a best-effort diagnostic: FIDES also uses +> defaults for unannotated tools, so no warning does not imply support or safety. +> Keep tools that require FIDES as direct agent tools, not Hyperlight-managed tools, +> and enforce authorization and destination restrictions inside host functions. +> Sandbox isolation does not supply FIDES enforcement. +> See the [FIDES guide](../../samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md). + ## Installation ```bash diff --git a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py index b28a2c4b267..45fe83f6b1c 100644 --- a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py +++ b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py @@ -4,6 +4,7 @@ import asyncio import contextvars +import logging import mimetypes import os import shutil @@ -32,9 +33,21 @@ from ._instructions import build_codeact_instructions, build_execute_code_description from ._types import AllowedDomain, AllowedDomainInput, FileMount, FileMountHostPath, FileMountInput +logger = logging.getLogger("agent_framework") + DEFAULT_HYPERLIGHT_BACKEND = "wasm" DEFAULT_HYPERLIGHT_MODULE = "python_guest.path" EXECUTE_CODE_TOOL_DESCRIPTION = "Execute Python in an isolated Hyperlight sandbox." +_FIDES_TOOL_PROPERTY_KEYS = frozenset({ + "source_integrity", + "confidentiality", + "max_allowed_confidentiality", + "accepts_untrusted", + "standing_guidance", + "agent_framework.security.principals", + "_agent_framework_internal_security_tool", + "_mcp_trust_server_ifc", +}) OUTPUT_FILE_RETRY_ATTEMPTS = 10 OUTPUT_FILE_RETRY_DELAY_SECONDS = 0.1 DEFAULT_MAX_OUTPUT_FILES = 20 @@ -1607,8 +1620,19 @@ def add_tools( ) -> None: """Add sandbox-managed tools to this execute_code surface.""" with self._state_lock: - combined_tools = _collect_tools(self._managed_tools, tools) + new_tools = _collect_tools(tools) + combined_tools = _collect_tools(self._managed_tools, new_tools) self._managed_tools = combined_tools + for tool_obj in new_tools: + if tool_obj.additional_properties and not _FIDES_TOOL_PROPERTY_KEYS.isdisjoint( + tool_obj.additional_properties + ): + logger.warning( + "FIDES is not supported with CodeAct providers. Metadata on tool '%s' will not be enforced " + "through function middleware for calls made inside generated code. " + "Keep FIDES-dependent tools as direct agent tools.", + tool_obj.name, + ) self._refresh_approval_mode() def get_tools(self) -> list[FunctionTool]: diff --git a/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py b/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py index 9af32e04054..fcec3c876e9 100644 --- a/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py +++ b/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py @@ -523,6 +523,130 @@ def test_execute_code_tool_updates_approval_with_managed_tools() -> None: assert execute_code.approval_mode == "always_require" +@pytest.mark.parametrize("entry_point", [HyperlightExecuteCodeTool, HyperlightCodeActProvider]) +@pytest.mark.parametrize("register_later", [False, True]) +@pytest.mark.parametrize( + "properties", + [ + {"source_integrity": "untrusted"}, + {"confidentiality": "private"}, + {"max_allowed_confidentiality": "public"}, + {"max_allowed_confidentiality": None}, + {"accepts_untrusted": False}, + {"standing_guidance": []}, + {"agent_framework.security.principals": []}, + {"_agent_framework_internal_security_tool": object()}, + {"_mcp_trust_server_ifc": False}, + ], +) +def test_codeact_warns_for_fides_tool_metadata( + entry_point: type[HyperlightExecuteCodeTool] | type[HyperlightCodeActProvider], + register_later: bool, + properties: dict[str, Any], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties=properties) + def annotated_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=None if register_later else annotated_tool, _registry=_FakeRuntime()) + if register_later: + codeact.add_tools(annotated_tool) + + records = [ + record for record in caplog.records if "FIDES is not supported with CodeAct providers." in record.message + ] + assert len(records) == 1 + assert records[0].levelname == "WARNING" + assert records[0].args == ("annotated_tool",) + assert codeact.get_tools()[0] is annotated_tool + assert annotated_tool.additional_properties == properties + assert annotated_tool.invocation_count == annotated_tool.invocation_exception_count == 0 + + +@pytest.mark.parametrize("entry_point", [HyperlightExecuteCodeTool, HyperlightCodeActProvider]) +@pytest.mark.parametrize("properties", [None, {}, {"custom_property": False}]) +def test_codeact_does_not_warn_for_unannotated_tools( + entry_point: type[HyperlightExecuteCodeTool] | type[HyperlightCodeActProvider], + properties: dict[str, Any] | None, + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties=properties) + def plain_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=plain_tool, _registry=_FakeRuntime()) + + assert "FIDES" not in caplog.text + assert codeact.get_tools()[0] is plain_tool + + +def test_run_snapshot_warns_for_fides_metadata_added_after_registration(caplog: pytest.LogCaptureFixture) -> None: + @tool + def annotated_tool(value: int) -> int: + return value + + codeact = HyperlightExecuteCodeTool(tools=annotated_tool, _registry=_FakeRuntime()) + assert annotated_tool.additional_properties is not None + annotated_tool.additional_properties["accepts_untrusted"] = False + + with caplog.at_level("WARNING", logger="agent_framework"): + run_tool = codeact.create_run_tool() + + assert "FIDES is not supported with CodeAct providers." in caplog.text + assert run_tool.get_tools()[0] is annotated_tool + assert annotated_tool.invocation_count == annotated_tool.invocation_exception_count == 0 + + +@pytest.mark.parametrize("entry_point", [HyperlightExecuteCodeTool, HyperlightCodeActProvider]) +def test_codeact_does_not_repeat_fides_warning_for_unrelated_registration( + entry_point: type[HyperlightExecuteCodeTool] | type[HyperlightCodeActProvider], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties={"source_integrity": "untrusted"}) + def annotated_tool(value: int) -> int: + return value + + @tool + def plain_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=annotated_tool, _registry=_FakeRuntime()) + caplog.clear() + codeact.add_tools(plain_tool) + + assert "FIDES" not in caplog.text + assert codeact.get_tools() == [annotated_tool, plain_tool] + + +@pytest.mark.parametrize("entry_point", [HyperlightExecuteCodeTool, HyperlightCodeActProvider]) +def test_codeact_warns_once_for_annotated_tool_replacement( + entry_point: type[HyperlightExecuteCodeTool] | type[HyperlightCodeActProvider], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(name="shared") + def original_tool(value: int) -> int: + return value + + @tool(name="shared", additional_properties={"accepts_untrusted": False}) + def replacement_tool(value: int) -> int: + return value + + codeact = entry_point(tools=original_tool, _registry=_FakeRuntime()) + with caplog.at_level("WARNING", logger="agent_framework"): + codeact.add_tools(replacement_tool) + + records = [ + record for record in caplog.records if "FIDES is not supported with CodeAct providers." in record.message + ] + assert len(records) == 1 + assert records[0].args == ("shared",) + assert codeact.get_tools() == [replacement_tool] + + def test_execute_code_tool_replaces_tools_with_the_same_name() -> None: execute_code = HyperlightExecuteCodeTool(tools=[compute], _registry=_FakeRuntime()) diff --git a/python/packages/monty/README.md b/python/packages/monty/README.md index ad4cab0a4a9..ad63ce90acf 100644 --- a/python/packages/monty/README.md +++ b/python/packages/monty/README.md @@ -6,6 +6,22 @@ Monty-backed CodeAct integrations for Microsoft Agent Framework. > This package is in **beta**. APIs may change before its stable release. It is > included in `agent-framework[all]`. +## FIDES compatibility + +> [!WARNING] +> **FIDES integration is not supported with CodeAct providers.** Host-tool calls +> made inside generated code bypass the agent's function middleware, so FIDES +> policy enforcement and label tracking do not apply to those nested calls or +> intermediate code values. Checks on the outer `execute_code` invocation do not +> enforce the policies of individual tools called inside it. +> +> Registering tools with recognized FIDES `additional_properties` logs a warning, +> but does not block execution. This is a best-effort diagnostic: FIDES also uses +> defaults for unannotated tools, so no warning does not imply support or safety. +> Keep tools that require FIDES as direct agent tools, not Monty-managed tools, and +> enforce authorization and destination restrictions inside host functions. +> See the [FIDES guide](../../samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md). + ## Installation ```bash diff --git a/python/packages/monty/agent_framework_monty/_execute_code_tool.py b/python/packages/monty/agent_framework_monty/_execute_code_tool.py index a4da33b364f..6a143f33bb0 100644 --- a/python/packages/monty/agent_framework_monty/_execute_code_tool.py +++ b/python/packages/monty/agent_framework_monty/_execute_code_tool.py @@ -13,6 +13,7 @@ from __future__ import annotations import json +import logging import mimetypes from collections.abc import Callable, Iterator, Sequence from functools import partial @@ -32,9 +33,22 @@ from ._monty_bridge import InlineCodeBridge, generate_type_stubs from ._types import FileMount, FileMountInput +logger = logging.getLogger("agent_framework") + EXECUTE_CODE_TOOL_NAME = "execute_code" EXECUTE_CODE_TOOL_DESCRIPTION = "Execute Python in a Monty interpreter." +_FIDES_TOOL_PROPERTY_KEYS = frozenset({ + "source_integrity", + "confidentiality", + "max_allowed_confidentiality", + "accepts_untrusted", + "standing_guidance", + "agent_framework.security.principals", + "_agent_framework_internal_security_tool", + "_mcp_trust_server_ifc", +}) + #: Virtual path that the optional ``workspace_root`` directory is mounted at, #: matching the Hyperlight default. Use ``file_mounts`` for any other path. WORKSPACE_MOUNT_PATH = "/input" @@ -258,7 +272,18 @@ def add_tools( tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]], ) -> None: """Add Monty-side tools to this execute_code surface.""" - self._managed_tools = _collect_tools(self._managed_tools, tools) + new_tools = _collect_tools(tools) + self._managed_tools = _collect_tools(self._managed_tools, new_tools) + for tool_obj in new_tools: + if tool_obj.additional_properties and not _FIDES_TOOL_PROPERTY_KEYS.isdisjoint( + tool_obj.additional_properties + ): + logger.warning( + "FIDES is not supported with CodeAct providers. Metadata on tool '%s' will not be enforced " + "through function middleware for calls made inside generated code. " + "Keep FIDES-dependent tools as direct agent tools.", + tool_obj.name, + ) self._refresh_approval_mode() def get_tools(self) -> list[FunctionTool]: diff --git a/python/packages/monty/tests/monty/test_monty_codeact.py b/python/packages/monty/tests/monty/test_monty_codeact.py index db1fc8c6750..33de9f603b6 100644 --- a/python/packages/monty/tests/monty/test_monty_codeact.py +++ b/python/packages/monty/tests/monty/test_monty_codeact.py @@ -262,6 +262,130 @@ def test_tool_construction_defaults() -> None: assert monty_tool.get_tools() == [] +@pytest.mark.parametrize("entry_point", [MontyExecuteCodeTool, MontyCodeActProvider]) +@pytest.mark.parametrize("register_later", [False, True]) +@pytest.mark.parametrize( + "properties", + [ + {"source_integrity": "untrusted"}, + {"confidentiality": "private"}, + {"max_allowed_confidentiality": "public"}, + {"max_allowed_confidentiality": None}, + {"accepts_untrusted": False}, + {"standing_guidance": []}, + {"agent_framework.security.principals": []}, + {"_agent_framework_internal_security_tool": object()}, + {"_mcp_trust_server_ifc": False}, + ], +) +def test_codeact_warns_for_fides_tool_metadata( + entry_point: type[MontyExecuteCodeTool] | type[MontyCodeActProvider], + register_later: bool, + properties: dict[str, Any], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties=properties) + def annotated_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=None if register_later else annotated_tool) + if register_later: + codeact.add_tools(annotated_tool) + + records = [ + record for record in caplog.records if "FIDES is not supported with CodeAct providers." in record.message + ] + assert len(records) == 1 + assert records[0].levelname == "WARNING" + assert records[0].args == ("annotated_tool",) + assert codeact.get_tools()[0] is annotated_tool + assert annotated_tool.additional_properties == properties + assert annotated_tool.invocation_count == annotated_tool.invocation_exception_count == 0 + + +@pytest.mark.parametrize("entry_point", [MontyExecuteCodeTool, MontyCodeActProvider]) +@pytest.mark.parametrize("properties", [None, {}, {"custom_property": False}]) +def test_codeact_does_not_warn_for_unannotated_tools( + entry_point: type[MontyExecuteCodeTool] | type[MontyCodeActProvider], + properties: dict[str, Any] | None, + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties=properties) + def plain_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=plain_tool) + + assert "FIDES" not in caplog.text + assert codeact.get_tools()[0] is plain_tool + + +def test_run_snapshot_warns_for_fides_metadata_added_after_registration(caplog: pytest.LogCaptureFixture) -> None: + @tool + def annotated_tool(value: int) -> int: + return value + + codeact = MontyExecuteCodeTool(tools=annotated_tool) + assert annotated_tool.additional_properties is not None + annotated_tool.additional_properties["accepts_untrusted"] = False + + with caplog.at_level("WARNING", logger="agent_framework"): + run_tool = codeact.create_run_tool() + + assert "FIDES is not supported with CodeAct providers." in caplog.text + assert run_tool.get_tools()[0] is annotated_tool + assert annotated_tool.invocation_count == annotated_tool.invocation_exception_count == 0 + + +@pytest.mark.parametrize("entry_point", [MontyExecuteCodeTool, MontyCodeActProvider]) +def test_codeact_does_not_repeat_fides_warning_for_unrelated_registration( + entry_point: type[MontyExecuteCodeTool] | type[MontyCodeActProvider], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(additional_properties={"source_integrity": "untrusted"}) + def annotated_tool(value: int) -> int: + return value + + @tool + def plain_tool(value: int) -> int: + return value + + with caplog.at_level("WARNING", logger="agent_framework"): + codeact = entry_point(tools=annotated_tool) + caplog.clear() + codeact.add_tools(plain_tool) + + assert "FIDES" not in caplog.text + assert codeact.get_tools() == [annotated_tool, plain_tool] + + +@pytest.mark.parametrize("entry_point", [MontyExecuteCodeTool, MontyCodeActProvider]) +def test_codeact_warns_once_for_annotated_tool_replacement( + entry_point: type[MontyExecuteCodeTool] | type[MontyCodeActProvider], + caplog: pytest.LogCaptureFixture, +) -> None: + @tool(name="shared") + def original_tool(value: int) -> int: + return value + + @tool(name="shared", additional_properties={"accepts_untrusted": False}) + def replacement_tool(value: int) -> int: + return value + + codeact = entry_point(tools=original_tool) + with caplog.at_level("WARNING", logger="agent_framework"): + codeact.add_tools(replacement_tool) + + records = [ + record for record in caplog.records if "FIDES is not supported with CodeAct providers." in record.message + ] + assert len(records) == 1 + assert records[0].args == ("shared",) + assert codeact.get_tools() == [replacement_tool] + + def test_add_remove_clear_tools_round_trip() -> None: monty_tool = MontyExecuteCodeTool() diff --git a/python/samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md b/python/samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md index fad09b01e88..b574ddd1154 100644 --- a/python/samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md +++ b/python/samples/02-agents/security/FIDES_DEVELOPER_GUIDE.md @@ -2,6 +2,23 @@ **FIDES** is a comprehensive security system for AI agents. This developer guide describes the deterministic prompt injection defense system implemented in the agent framework. The system provides label-based security mechanisms to defend against prompt injection attacks by tracking integrity and confidentiality of content throughout agent execution. +> [!WARNING] +> **Combining FIDES with CodeAct providers is currently unsupported**, including +> [Monty](../../../packages/monty/README.md) and +> [Hyperlight](../../../packages/hyperlight/README.md). FIDES's per-tool guarantees +> apply to invocations routed through its function middleware. Host-tool calls +> inside generated code bypass that middleware; nested calls and intermediate +> code values do not receive its policy enforcement or label tracking. Checking +> the outer `execute_code` invocation does not enforce the policies of individual +> tools called inside it, and sandbox isolation does not supply FIDES enforcement. +> +> CodeAct logs a warning for recognized FIDES tool metadata, but this is diagnostic +> only: execution is not blocked, and defaults can apply to unannotated tools. +> No warning does not imply support or safety. Keep tools that require FIDES as +> direct agent tools rather than CodeAct-managed tools, or disable CodeAct for +> workflows requiring those guarantees. Host functions must independently enforce +> authorization and destination restrictions. + ## Context Provider Pattern with SecureAgentConfig! **`SecureAgentConfig` is now a `ContextProvider`** — add it to any agent with a single `context_providers=[config]` line. It automatically injects security tools, instructions, and middleware via the `before_run()` hook. No security knowledge required from developers.