@macaw-ai/client 0.0.0-stage → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +339 -0
- package/bin/install.js +422 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW Adapter for Pydantic AI
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for pydantic_ai.Agent. Model calls become
|
|
5
|
+
tool:<app>/generate and tool calls become tool:<app>/<tool_name>, so both are
|
|
6
|
+
governed by the MAPL policies you already write.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from macaw_adapters.pydantic_ai import SecureAgent
|
|
10
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
11
|
+
|
|
12
|
+
agent = SecureAgent(
|
|
13
|
+
OpenAIChatModel("gpt-4o"),
|
|
14
|
+
app_name="acme",
|
|
15
|
+
tools=[query_catalog, get_lineage],
|
|
16
|
+
)
|
|
17
|
+
result = await agent.run("which tables contain PII?")
|
|
18
|
+
|
|
19
|
+
Multi-user:
|
|
20
|
+
alice_agent = agent.bind_to_user(alice_client)
|
|
21
|
+
|
|
22
|
+
Prerequisites:
|
|
23
|
+
- MACAW Client Library: https://console.macawsecurity.ai
|
|
24
|
+
- pip install macaw-adapters[pydantic-ai]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from macaw_adapters.pydantic_ai.agent import Agent, SecureAgent
|
|
28
|
+
from macaw_adapters.pydantic_ai.model import SecureModel
|
|
29
|
+
from macaw_adapters.pydantic_ai.toolset import SecureToolset
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"SecureAgent",
|
|
33
|
+
"Agent", # drop-in alias
|
|
34
|
+
"SecureModel",
|
|
35
|
+
"SecureToolset",
|
|
36
|
+
]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared MACAW binding for the Pydantic AI adapter.
|
|
3
|
+
|
|
4
|
+
MacawBinding holds the MACAWClient, the set of registered resource names, and
|
|
5
|
+
the models reachable by the generate handler. SecureModel and SecureToolset are
|
|
6
|
+
constructed with a binding and call through it.
|
|
7
|
+
|
|
8
|
+
MacawBinding(app_name=...) registers the app; issues as the app
|
|
9
|
+
binding.for_user(user_client) shares that registration; issues as the user
|
|
10
|
+
|
|
11
|
+
binding.register_tool(name, fn) -> tool:<app>/<name>, dispatched by MACAW
|
|
12
|
+
binding.register_ack(name) -> tool:<app>/<name>, authorization only
|
|
13
|
+
binding.invoke(resource, params) -> invoke_tool(tool:<app>/<resource>, ...)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import inspect
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, Callable, Dict, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Carries ModelRequestParameters through invoke_tool. invoke_tool has no channel
|
|
24
|
+
# other than `parameters`, so this rides alongside the policy inputs.
|
|
25
|
+
REQUEST_PARAMETERS_KEY = "_request_parameters"
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from macaw_client import MACAWClient, PermissionDenied
|
|
29
|
+
except ImportError: # pragma: no cover - surfaced at construction time
|
|
30
|
+
MACAWClient = None
|
|
31
|
+
PermissionDenied = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_permission_denied(exc: BaseException) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
True if `exc` is a MACAW policy denial.
|
|
37
|
+
|
|
38
|
+
macaw_client exports one PermissionDenied, and a second class of the same
|
|
39
|
+
name is raised from the SDK core. Match on the name so a denial is never
|
|
40
|
+
mistaken for a transport error.
|
|
41
|
+
"""
|
|
42
|
+
if PermissionDenied is not None and isinstance(exc, PermissionDenied):
|
|
43
|
+
return True
|
|
44
|
+
return type(exc).__name__ == "PermissionDenied"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def as_handler(fn: Callable) -> Callable[[Dict[str, Any]], Any]:
|
|
48
|
+
"""
|
|
49
|
+
Adapt a tool function into a MACAW handler.
|
|
50
|
+
|
|
51
|
+
Handlers receive a params dict and run on MACAW's handler pool after the PEP
|
|
52
|
+
has allowed the call. Async tools are driven to completion there - the pool
|
|
53
|
+
thread has no event loop of its own.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def handler(params: Dict[str, Any]) -> Any:
|
|
57
|
+
result = fn(**params)
|
|
58
|
+
if inspect.isawaitable(result):
|
|
59
|
+
result = asyncio.run(result)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
return handler
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _acknowledge(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
66
|
+
"""Handler for resources MACAW authorizes but does not execute."""
|
|
67
|
+
return {"authorized": True}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MacawBinding:
|
|
71
|
+
"""Owns the MACAWClient and turns every seam into an invoke_tool call."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
app_name: Optional[str] = None,
|
|
76
|
+
intent_policy: Optional[Dict[str, Any]] = None,
|
|
77
|
+
jwt_token: Optional[str] = None,
|
|
78
|
+
user_name: Optional[str] = None,
|
|
79
|
+
):
|
|
80
|
+
if MACAWClient is None:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"MACAWClient not installed. Download from https://console.macawsecurity.ai"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.app_name = app_name or "secure-pydantic-app"
|
|
86
|
+
self._mode = "user" if jwt_token else "service"
|
|
87
|
+
self._registered = set()
|
|
88
|
+
self._models = {} # model_name -> wrapped pydantic_ai Model
|
|
89
|
+
|
|
90
|
+
# The model seam. Declaring "messages" as a prompt is what makes
|
|
91
|
+
# invoke_tool create authenticated prompts for the conversation.
|
|
92
|
+
self.tools = {
|
|
93
|
+
f"tool:{self.app_name}/generate": {
|
|
94
|
+
"handler": self._handle_generate,
|
|
95
|
+
"prompts": ["messages"],
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
identity = (
|
|
100
|
+
{"user_name": user_name, "iam_token": jwt_token, "agent_type": "user"}
|
|
101
|
+
if jwt_token
|
|
102
|
+
else {}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.macaw_client = MACAWClient(
|
|
106
|
+
app_name=self.app_name,
|
|
107
|
+
app_version="1.0.0",
|
|
108
|
+
intent_policy=intent_policy or {},
|
|
109
|
+
tools=self.tools,
|
|
110
|
+
**identity,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if self.macaw_client.register():
|
|
114
|
+
self.server_id = self.macaw_client.agent_id
|
|
115
|
+
logger.info(
|
|
116
|
+
"SecureAgent registered as %s (mode: %s)", self.server_id, self._mode
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
raise RuntimeError("Failed to register with MACAW LocalAgent")
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ users
|
|
122
|
+
|
|
123
|
+
def for_user(self, user_client: Any) -> "UserBinding":
|
|
124
|
+
"""Return a binding that issues invocations as `user_client`."""
|
|
125
|
+
if self._mode != "service":
|
|
126
|
+
raise ValueError("bind_to_user() is only valid on a service-mode agent")
|
|
127
|
+
if not hasattr(user_client, "agent_id") or not hasattr(user_client, "invoke_tool"):
|
|
128
|
+
raise ValueError("bind_to_user() requires a valid MACAWClient instance")
|
|
129
|
+
if not getattr(user_client, "registered", False):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"bind_to_user() requires a registered MACAWClient. Call register() first."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
agent_type = getattr(user_client, "agent_type", None)
|
|
135
|
+
if agent_type and agent_type != "user":
|
|
136
|
+
logger.warning(
|
|
137
|
+
"bind_to_user() called with agent_type=%r (expected 'user'). "
|
|
138
|
+
"User identity and policy enforcement may not work as expected.",
|
|
139
|
+
agent_type,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return UserBinding(self, user_client)
|
|
143
|
+
|
|
144
|
+
# -------------------------------------------------------------- resources
|
|
145
|
+
|
|
146
|
+
def register_tool(self, name: str, fn: Callable) -> str:
|
|
147
|
+
"""Register a callable MACAW will dispatch."""
|
|
148
|
+
return self._register(name, as_handler(fn))
|
|
149
|
+
|
|
150
|
+
def register_ack(self, name: str) -> str:
|
|
151
|
+
"""Register a resource MACAW authorizes but does not execute."""
|
|
152
|
+
return self._register(name, _acknowledge)
|
|
153
|
+
|
|
154
|
+
def register_model(self, wrapped) -> None:
|
|
155
|
+
"""Make `wrapped` reachable by the tool:<app>/generate handler."""
|
|
156
|
+
self._models[wrapped.model_name] = wrapped
|
|
157
|
+
|
|
158
|
+
def _register(self, name: str, handler: Callable) -> str:
|
|
159
|
+
mapl_name = f"tool:{self.app_name}/{name}"
|
|
160
|
+
if mapl_name not in self._registered:
|
|
161
|
+
self.macaw_client.register_tool(mapl_name, handler)
|
|
162
|
+
self._registered.add(mapl_name)
|
|
163
|
+
logger.info("Registered resource: %s", mapl_name)
|
|
164
|
+
return mapl_name
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------- invocation
|
|
167
|
+
|
|
168
|
+
def invoke(self, resource: str, params: Dict[str, Any], **kwargs) -> Any:
|
|
169
|
+
"""
|
|
170
|
+
Invoke a resource through MACAW.
|
|
171
|
+
|
|
172
|
+
Blocking: policy, signing, and audit all happen inside this call. Callers
|
|
173
|
+
on an event loop must run it off-thread.
|
|
174
|
+
"""
|
|
175
|
+
return self.issuer.invoke_tool(
|
|
176
|
+
tool_name=f"tool:{self.app_name}/{resource}",
|
|
177
|
+
parameters=params,
|
|
178
|
+
target_agent=self.server_id,
|
|
179
|
+
**kwargs,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def issuer(self):
|
|
184
|
+
"""Client that issues invocations. UserBinding overrides this."""
|
|
185
|
+
return self.macaw_client
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------ model seam
|
|
188
|
+
|
|
189
|
+
def _handle_generate(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
190
|
+
"""
|
|
191
|
+
Handler for tool:<app>/generate.
|
|
192
|
+
|
|
193
|
+
Runs on MACAW's handler pool once the PEP allows the call, so the
|
|
194
|
+
provider request and its response both pass through invoke_tool - the
|
|
195
|
+
same path an owned tool call takes.
|
|
196
|
+
|
|
197
|
+
Streaming authorizes here and streams from the wrapped model in the
|
|
198
|
+
caller, so no stream event is re-serialized on the way back.
|
|
199
|
+
"""
|
|
200
|
+
from pydantic import TypeAdapter
|
|
201
|
+
from pydantic_ai.messages import ModelMessagesTypeAdapter, ModelResponse
|
|
202
|
+
from pydantic_ai.models import ModelRequestParameters
|
|
203
|
+
|
|
204
|
+
p = dict(params)
|
|
205
|
+
if p.get("stream"):
|
|
206
|
+
return {"authorized": True}
|
|
207
|
+
|
|
208
|
+
model_name = p.pop("model")
|
|
209
|
+
messages = ModelMessagesTypeAdapter.validate_python(p.pop("messages"))
|
|
210
|
+
request_parameters = TypeAdapter(ModelRequestParameters).validate_python(
|
|
211
|
+
p.pop(REQUEST_PARAMETERS_KEY)
|
|
212
|
+
)
|
|
213
|
+
p.pop("native_tools", None) # policy input; not a model setting
|
|
214
|
+
|
|
215
|
+
wrapped = self._models.get(model_name)
|
|
216
|
+
if wrapped is None:
|
|
217
|
+
raise ValueError(f"No model registered under {model_name!r}")
|
|
218
|
+
|
|
219
|
+
response = asyncio.run(wrapped.request(messages, p or None, request_parameters))
|
|
220
|
+
return TypeAdapter(ModelResponse).dump_python(response, mode="json")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class UserBinding:
|
|
224
|
+
"""A MacawBinding that issues invocations as a specific user."""
|
|
225
|
+
|
|
226
|
+
def __init__(self, base: MacawBinding, user_client: Any):
|
|
227
|
+
self._base = base
|
|
228
|
+
self._user_client = user_client
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def issuer(self):
|
|
232
|
+
return self._user_client
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def app_name(self) -> str:
|
|
236
|
+
return self._base.app_name
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def server_id(self) -> str:
|
|
240
|
+
return self._base.server_id
|
|
241
|
+
|
|
242
|
+
def register_tool(self, name: str, fn: Callable) -> str:
|
|
243
|
+
return self._base.register_tool(name, fn)
|
|
244
|
+
|
|
245
|
+
def register_ack(self, name: str) -> str:
|
|
246
|
+
return self._base.register_ack(name)
|
|
247
|
+
|
|
248
|
+
def register_model(self, wrapped) -> None:
|
|
249
|
+
self._base.register_model(wrapped)
|
|
250
|
+
|
|
251
|
+
def invoke(self, resource: str, params: Dict[str, Any], **kwargs) -> Any:
|
|
252
|
+
return self._user_client.invoke_tool(
|
|
253
|
+
tool_name=f"tool:{self.app_name}/{resource}",
|
|
254
|
+
parameters=params,
|
|
255
|
+
target_agent=self.server_id,
|
|
256
|
+
**kwargs,
|
|
257
|
+
)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecureAgent - drop-in replacement for pydantic_ai.Agent.
|
|
3
|
+
|
|
4
|
+
# Before
|
|
5
|
+
from pydantic_ai import Agent
|
|
6
|
+
agent = Agent(OpenAIChatModel("gpt-4o"), tools=[query_catalog])
|
|
7
|
+
|
|
8
|
+
# After
|
|
9
|
+
from macaw_adapters.pydantic_ai import SecureAgent
|
|
10
|
+
agent = SecureAgent(OpenAIChatModel("gpt-4o"), app_name="acme",
|
|
11
|
+
tools=[query_catalog])
|
|
12
|
+
|
|
13
|
+
Every way Pydantic AI takes tools is governed:
|
|
14
|
+
|
|
15
|
+
tools=[fn] registered with MACAW
|
|
16
|
+
@agent.tool / @agent.tool_plain registered with MACAW
|
|
17
|
+
toolsets=[FunctionToolset(...)] functions registered with MACAW
|
|
18
|
+
toolsets=[MCPToolset(...)] authorized here, executed by the server it
|
|
19
|
+
connects to (governed there by SecureMCP or
|
|
20
|
+
SecureMCPProxy)
|
|
21
|
+
|
|
22
|
+
run(), run_sync(), run_stream(), and override() wrap any model or toolsets
|
|
23
|
+
passed to them, so a per-call override cannot route around the adapter.
|
|
24
|
+
|
|
25
|
+
Multi-user:
|
|
26
|
+
|
|
27
|
+
alice_agent = agent.bind_to_user(alice_client)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from typing import Any, Callable, List, Optional, Sequence
|
|
33
|
+
|
|
34
|
+
from pydantic_ai import Agent as _Agent
|
|
35
|
+
from pydantic_ai.toolsets import AbstractToolset, FunctionToolset
|
|
36
|
+
|
|
37
|
+
from macaw_adapters.pydantic_ai._core import MacawBinding
|
|
38
|
+
from macaw_adapters.pydantic_ai.model import SecureModel
|
|
39
|
+
from macaw_adapters.pydantic_ai.toolset import SecureToolset
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _functions_of(toolset: AbstractToolset) -> Optional[List[Callable]]:
|
|
45
|
+
"""
|
|
46
|
+
The plain functions behind a toolset, or None if MACAW cannot hold them.
|
|
47
|
+
|
|
48
|
+
FunctionToolset exposes `.tools` as {name: Tool}, with Tool.function being
|
|
49
|
+
the original callable.
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(toolset, FunctionToolset):
|
|
52
|
+
return None
|
|
53
|
+
return [t.function for t in toolset.tools.values() if hasattr(t, "function")]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SecureAgent(_Agent):
|
|
57
|
+
"""A pydantic_ai.Agent whose model and tool calls route through MACAW."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
model=None,
|
|
62
|
+
*,
|
|
63
|
+
app_name: Optional[str] = None,
|
|
64
|
+
intent_policy: Optional[dict] = None,
|
|
65
|
+
jwt_token: Optional[str] = None,
|
|
66
|
+
user_name: Optional[str] = None,
|
|
67
|
+
tools: Sequence[Callable] = (),
|
|
68
|
+
toolsets: Optional[Sequence[AbstractToolset]] = None,
|
|
69
|
+
_binding: Optional[MacawBinding] = None,
|
|
70
|
+
**kwargs,
|
|
71
|
+
):
|
|
72
|
+
self._macaw = _binding or MacawBinding(
|
|
73
|
+
app_name=app_name,
|
|
74
|
+
intent_policy=intent_policy,
|
|
75
|
+
jwt_token=jwt_token,
|
|
76
|
+
user_name=user_name,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Kept so bind_to_user() can rebuild an identical agent on a user binding.
|
|
80
|
+
self._raw_model = model
|
|
81
|
+
self._raw_tools = list(tools)
|
|
82
|
+
self._raw_toolsets = list(toolsets or [])
|
|
83
|
+
self._raw_kwargs = dict(kwargs)
|
|
84
|
+
|
|
85
|
+
# tools= and the @agent.tool decorators share one governed toolset.
|
|
86
|
+
functions = list(tools)
|
|
87
|
+
passthrough = []
|
|
88
|
+
for toolset in self._raw_toolsets:
|
|
89
|
+
fns = _functions_of(toolset)
|
|
90
|
+
if fns is None:
|
|
91
|
+
passthrough.append(toolset)
|
|
92
|
+
else:
|
|
93
|
+
functions.extend(fns)
|
|
94
|
+
|
|
95
|
+
self._tools = SecureToolset(
|
|
96
|
+
self._macaw, FunctionToolset(tools=functions), owned=functions
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
super().__init__(
|
|
100
|
+
self._secure_model(model),
|
|
101
|
+
toolsets=[self._tools] + [self._secure_toolset(t) for t in passthrough],
|
|
102
|
+
**kwargs,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# --------------------------------------------------------------- wrapping
|
|
106
|
+
|
|
107
|
+
def _secure_model(self, model):
|
|
108
|
+
if model is None or isinstance(model, SecureModel):
|
|
109
|
+
return model
|
|
110
|
+
if isinstance(model, str):
|
|
111
|
+
from pydantic_ai.models import infer_model
|
|
112
|
+
|
|
113
|
+
model = infer_model(model)
|
|
114
|
+
|
|
115
|
+
# A router such as FallbackModel keeps its candidates in .models and
|
|
116
|
+
# reports a composite model_name. Secure the candidates instead, so
|
|
117
|
+
# policy always sees the model that actually calls a provider.
|
|
118
|
+
members = getattr(model, "models", None)
|
|
119
|
+
if members:
|
|
120
|
+
model.models = [self._secure_model(m) for m in members]
|
|
121
|
+
return model
|
|
122
|
+
|
|
123
|
+
return SecureModel(self._macaw, model)
|
|
124
|
+
|
|
125
|
+
def _secure_toolset(self, toolset: AbstractToolset):
|
|
126
|
+
if isinstance(toolset, SecureToolset):
|
|
127
|
+
return toolset
|
|
128
|
+
fns = _functions_of(toolset)
|
|
129
|
+
if fns is None:
|
|
130
|
+
return SecureToolset(self._macaw, toolset)
|
|
131
|
+
return SecureToolset(self._macaw, toolset, owned=fns)
|
|
132
|
+
|
|
133
|
+
def _secure_toolsets(self, toolsets):
|
|
134
|
+
if toolsets is None:
|
|
135
|
+
return None
|
|
136
|
+
return [self._secure_toolset(t) for t in toolsets]
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------- decorators
|
|
139
|
+
|
|
140
|
+
def tool(self, func=None, /, **kwargs):
|
|
141
|
+
"""Register a tool that takes RunContext. Governed like any other."""
|
|
142
|
+
return self._tool_decorator(func, takes_ctx=True, **kwargs)
|
|
143
|
+
|
|
144
|
+
def tool_plain(self, func=None, /, **kwargs):
|
|
145
|
+
"""Register a tool that does not take RunContext."""
|
|
146
|
+
return self._tool_decorator(func, takes_ctx=False, **kwargs)
|
|
147
|
+
|
|
148
|
+
def _tool_decorator(self, func, *, takes_ctx: bool, **kwargs):
|
|
149
|
+
def register(fn):
|
|
150
|
+
self._tools.wrapped.add_function(fn, takes_ctx=takes_ctx, **kwargs)
|
|
151
|
+
name = kwargs.get("name") or fn.__name__
|
|
152
|
+
if takes_ctx:
|
|
153
|
+
# A tool taking RunContext cannot be dispatched by MACAW, which
|
|
154
|
+
# passes only a params dict, so it is authorized instead.
|
|
155
|
+
self._macaw.register_ack(name)
|
|
156
|
+
else:
|
|
157
|
+
self._tools.own(fn, name=name)
|
|
158
|
+
return fn
|
|
159
|
+
|
|
160
|
+
return register if func is None else register(func)
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------ seams
|
|
163
|
+
# Per-call model/toolsets replace the agent's own, so they are wrapped too.
|
|
164
|
+
|
|
165
|
+
async def run(self, *args, model=None, toolsets=None, **kwargs):
|
|
166
|
+
return await super().run(
|
|
167
|
+
*args,
|
|
168
|
+
model=self._secure_model(model),
|
|
169
|
+
toolsets=self._secure_toolsets(toolsets),
|
|
170
|
+
**kwargs,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def run_sync(self, *args, model=None, toolsets=None, **kwargs):
|
|
174
|
+
return super().run_sync(
|
|
175
|
+
*args,
|
|
176
|
+
model=self._secure_model(model),
|
|
177
|
+
toolsets=self._secure_toolsets(toolsets),
|
|
178
|
+
**kwargs,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def run_stream(self, *args, model=None, toolsets=None, **kwargs):
|
|
182
|
+
return super().run_stream(
|
|
183
|
+
*args,
|
|
184
|
+
model=self._secure_model(model),
|
|
185
|
+
toolsets=self._secure_toolsets(toolsets),
|
|
186
|
+
**kwargs,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@contextmanager
|
|
190
|
+
def override(self, *, model=None, toolsets=None, **kwargs):
|
|
191
|
+
overrides = dict(kwargs)
|
|
192
|
+
if model is not None:
|
|
193
|
+
overrides["model"] = self._secure_model(model)
|
|
194
|
+
if toolsets is not None:
|
|
195
|
+
overrides["toolsets"] = self._secure_toolsets(toolsets)
|
|
196
|
+
with super().override(**overrides):
|
|
197
|
+
yield
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------ users
|
|
200
|
+
|
|
201
|
+
def bind_to_user(self, user_client: Any) -> "SecureAgent":
|
|
202
|
+
"""
|
|
203
|
+
Return an agent that issues invocations as `user_client`.
|
|
204
|
+
|
|
205
|
+
Shares this agent's registration and tools; policy is evaluated for the
|
|
206
|
+
user on every model and tool call.
|
|
207
|
+
"""
|
|
208
|
+
return SecureAgent(
|
|
209
|
+
self._raw_model,
|
|
210
|
+
tools=self._raw_tools,
|
|
211
|
+
toolsets=self._raw_toolsets,
|
|
212
|
+
_binding=self._macaw.for_user(user_client),
|
|
213
|
+
**self._raw_kwargs,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Drop-in alias (same class, native naming)
|
|
218
|
+
Agent = SecureAgent
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecureModel - the model seam.
|
|
3
|
+
|
|
4
|
+
Wraps any Pydantic AI model so every request is invoked as tool:<app>/generate.
|
|
5
|
+
MACAW dispatches to the wrapped model once the PEP allows the call, so the
|
|
6
|
+
request and the response both travel through invoke_tool - the same path a tool
|
|
7
|
+
call takes.
|
|
8
|
+
|
|
9
|
+
What policy sees per call:
|
|
10
|
+
|
|
11
|
+
model the model id, e.g. "anthropic.claude-haiku-4-5"
|
|
12
|
+
messages the conversation, declared as a prompt so invoke_tool creates
|
|
13
|
+
authenticated prompts from it
|
|
14
|
+
native_tools provider-side tools offered on this call
|
|
15
|
+
<settings> max_tokens, temperature, and anything else the caller set
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
import anyio
|
|
23
|
+
from pydantic import TypeAdapter
|
|
24
|
+
from pydantic_ai.messages import ModelMessagesTypeAdapter, ModelResponse
|
|
25
|
+
from pydantic_ai.models import ModelRequestParameters
|
|
26
|
+
from pydantic_ai.models.wrapper import WrapperModel
|
|
27
|
+
|
|
28
|
+
from macaw_adapters.pydantic_ai._core import REQUEST_PARAMETERS_KEY
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_RESPONSE = TypeAdapter(ModelResponse)
|
|
33
|
+
_REQUEST_PARAMETERS = TypeAdapter(ModelRequestParameters)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SecureModel(WrapperModel):
|
|
37
|
+
"""A Pydantic AI model whose every request goes through the MACAW PEP."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, binding, wrapped):
|
|
40
|
+
super().__init__(wrapped)
|
|
41
|
+
self._macaw = binding
|
|
42
|
+
binding.register_model(self.wrapped)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _native_tool_names(request_parameters) -> List[str]:
|
|
46
|
+
"""
|
|
47
|
+
Names of the provider-side tools offered on this request.
|
|
48
|
+
|
|
49
|
+
Read from the request rather than from what the application declared, so
|
|
50
|
+
policy sees what the provider was actually asked for.
|
|
51
|
+
"""
|
|
52
|
+
native = getattr(request_parameters, "native_tools", None) or []
|
|
53
|
+
return sorted(
|
|
54
|
+
getattr(tool, "kind", None) or type(tool).__name__ for tool in native
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _params(
|
|
58
|
+
self,
|
|
59
|
+
messages: List[Any],
|
|
60
|
+
model_settings: Optional[Dict[str, Any]],
|
|
61
|
+
request_parameters: ModelRequestParameters,
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"model": self.wrapped.model_name,
|
|
65
|
+
"messages": ModelMessagesTypeAdapter.dump_python(messages, mode="json"),
|
|
66
|
+
"native_tools": self._native_tool_names(request_parameters),
|
|
67
|
+
REQUEST_PARAMETERS_KEY: _REQUEST_PARAMETERS.dump_python(
|
|
68
|
+
request_parameters, mode="json"
|
|
69
|
+
),
|
|
70
|
+
**(model_settings or {}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async def request(self, messages, model_settings, model_request_parameters):
|
|
74
|
+
params = self._params(messages, model_settings, model_request_parameters)
|
|
75
|
+
result = await anyio.to_thread.run_sync(
|
|
76
|
+
lambda: self._macaw.invoke("generate", params)
|
|
77
|
+
)
|
|
78
|
+
return _RESPONSE.validate_python(result)
|
|
79
|
+
|
|
80
|
+
@asynccontextmanager
|
|
81
|
+
async def request_stream(
|
|
82
|
+
self, messages, model_settings, model_request_parameters, run_context=None
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Streaming path.
|
|
86
|
+
|
|
87
|
+
The call is authorized through invoke_tool and the events are then
|
|
88
|
+
streamed from the wrapped model directly, so no stream event is
|
|
89
|
+
re-serialized on the way to the caller.
|
|
90
|
+
"""
|
|
91
|
+
params = self._params(messages, model_settings, model_request_parameters)
|
|
92
|
+
params["stream"] = True
|
|
93
|
+
await anyio.to_thread.run_sync(
|
|
94
|
+
lambda: self._macaw.invoke("generate", dict(params, _authorize_only=True))
|
|
95
|
+
)
|
|
96
|
+
async with self.wrapped.request_stream(
|
|
97
|
+
messages, model_settings, model_request_parameters, run_context
|
|
98
|
+
) as stream:
|
|
99
|
+
yield stream
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecureToolset - the tool seam.
|
|
3
|
+
|
|
4
|
+
Wraps a Pydantic AI toolset so every call is invoked as tool:<app>/<tool_name>.
|
|
5
|
+
|
|
6
|
+
Two cases, decided by whether MACAW can hold the callable:
|
|
7
|
+
|
|
8
|
+
owned plain functions (tools=, @agent.tool_plain, FunctionToolset).
|
|
9
|
+
Registered with MACAW, which dispatches them - request and
|
|
10
|
+
response both travel through invoke_tool.
|
|
11
|
+
|
|
12
|
+
not owned toolsets whose execution MACAW cannot hold, such as MCPToolset
|
|
13
|
+
or a tool taking RunContext. The call is authorized through
|
|
14
|
+
invoke_tool and then executed by the wrapped toolset.
|
|
15
|
+
|
|
16
|
+
Either way no tool runs until the PEP allows it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
21
|
+
|
|
22
|
+
import anyio
|
|
23
|
+
from pydantic_ai.exceptions import ToolFailed
|
|
24
|
+
from pydantic_ai.toolsets import AbstractToolset, WrapperToolset
|
|
25
|
+
|
|
26
|
+
from macaw_adapters.pydantic_ai._core import is_permission_denied
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SecureToolset(WrapperToolset):
|
|
32
|
+
"""A Pydantic AI toolset whose every call goes through the MACAW PEP."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
binding,
|
|
37
|
+
wrapped: AbstractToolset,
|
|
38
|
+
owned: Iterable[Callable] = (),
|
|
39
|
+
):
|
|
40
|
+
super().__init__(wrapped)
|
|
41
|
+
self._macaw = binding
|
|
42
|
+
self._owned = set()
|
|
43
|
+
for fn in owned:
|
|
44
|
+
self.own(fn)
|
|
45
|
+
|
|
46
|
+
def own(self, fn: Callable, name: Optional[str] = None) -> Callable:
|
|
47
|
+
"""Register `fn` with MACAW so MACAW dispatches it."""
|
|
48
|
+
tool_name = name or fn.__name__
|
|
49
|
+
self._macaw.register_tool(tool_name, fn)
|
|
50
|
+
self._owned.add(tool_name)
|
|
51
|
+
return fn
|
|
52
|
+
|
|
53
|
+
async def get_tools(self, ctx) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Resolve tools from the wrapped toolset and make each one a MACAW
|
|
56
|
+
resource. Schemas and validation stay Pydantic AI's.
|
|
57
|
+
"""
|
|
58
|
+
tools = await super().get_tools(ctx)
|
|
59
|
+
for name in tools:
|
|
60
|
+
if name not in self._owned:
|
|
61
|
+
self._macaw.register_ack(name)
|
|
62
|
+
return tools
|
|
63
|
+
|
|
64
|
+
async def call_tool(self, name: str, tool_args: Dict[str, Any], ctx, tool) -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Invoke through MACAW. Owned tools are dispatched by MACAW; the rest are
|
|
67
|
+
authorized here and executed by the wrapped toolset.
|
|
68
|
+
|
|
69
|
+
A denial is re-raised as ToolFailed so the model sees it and adapts.
|
|
70
|
+
Retrying would not change the outcome, which is why this is ToolFailed
|
|
71
|
+
rather than ModelRetry.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
result = await anyio.to_thread.run_sync(
|
|
75
|
+
lambda: self._macaw.invoke(name, tool_args)
|
|
76
|
+
)
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
if is_permission_denied(exc):
|
|
79
|
+
raise ToolFailed(
|
|
80
|
+
f"Access denied by security policy: {name}. {exc}"
|
|
81
|
+
) from exc
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
if name in self._owned:
|
|
85
|
+
return result
|
|
86
|
+
return await self.wrapped.call_tool(name, tool_args, ctx, tool)
|