@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.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +422 -0
  3. package/package.json +37 -4
  4. package/payload/README.md +56 -0
  5. package/payload/secureAI/LICENSE +190 -0
  6. package/payload/secureAI/NOTICE +5 -0
  7. package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
  8. package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
  9. package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
  10. package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
  11. package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
  12. package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
  13. package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
  14. package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
  15. package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
  16. package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
  17. package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
  18. package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
  19. package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
  20. package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
  21. package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
  22. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
  23. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
  24. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
  25. package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
  26. package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
  27. package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
  28. package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
  29. package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
  30. package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
  31. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
  32. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
  33. package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
  34. package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
  35. package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
  36. package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
  37. package/payload/secureAI/examples/README.md +244 -0
  38. package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
  39. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
  40. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
  41. package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
  42. package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
  43. package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
  44. package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
  45. package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
  46. package/payload/secureAI/examples/attestations/README.md +128 -0
  47. package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
  48. package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
  49. package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
  50. package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
  51. package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
  52. package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
  53. package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
  54. package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
  55. package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
  56. package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
  57. package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
  58. package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
  59. package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
  60. package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
  61. package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
  62. package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
  63. package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
  64. package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
  65. package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
  66. package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
  67. package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
  68. package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
  69. package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
  70. package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
  71. package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
  72. package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
  73. package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
  74. package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
  75. package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
  76. package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
  77. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
  78. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
  79. package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
  80. package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
  81. package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
  82. package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
  83. package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
  84. package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
  85. package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
  86. package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
  87. package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
  88. package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
  89. package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
  90. package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
  91. package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
  92. package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
  93. package/payload/secureAI/macaw_adapters/__init__.py +55 -0
  94. package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
  95. package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
  96. package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
  97. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
  98. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
  99. package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
  100. package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
  101. package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
  102. package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
  103. package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
  104. package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
  105. package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
  106. package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
  107. package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
  108. package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
  109. package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
  110. package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
  111. package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
  112. package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
  113. package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
  114. package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
  115. package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
  116. package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
  117. package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
  118. package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
  119. package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
  120. package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
  121. package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
  122. package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
  123. package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
  124. package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
  125. package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
  126. package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
  127. package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
  128. package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
  129. package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
  130. package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
  131. package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
  132. package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
  133. package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
  134. package/payload/secureAI/test_harness_quick.py +296 -0
  135. package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
  136. package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
  137. package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
  138. package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
  139. package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
  140. package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
  141. package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
  142. 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)