@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,410 @@
1
+ """
2
+ MACAW ChatOpenAI - Drop-in replacement for langchain_openai.ChatOpenAI with MACAW protection.
3
+
4
+ Usage:
5
+ # Instead of: from langchain_openai import ChatOpenAI
6
+ from macaw_adapters.langchain.openai import ChatOpenAI
7
+
8
+ # Same API, MACAW security is invisible
9
+ llm = ChatOpenAI(model="gpt-4")
10
+ response = llm.invoke("Hello, world!")
11
+
12
+ Security:
13
+ This adapter uses SecureOpenAI internally, which routes all LLM calls through
14
+ MACAW's Policy Enforcement Point (PEP) via invoke_tool. This ensures:
15
+ - Policy enforcement on every LLM call
16
+ - Cryptographic audit logging
17
+ - Per-user identity propagation via bind_to_user
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Dict, Iterator, List, Optional
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Global registry of ChatOpenAI instances for cleanup
26
+ _instances: List['ChatOpenAI'] = []
27
+
28
+
29
+ class ChatOpenAI:
30
+ """
31
+ Drop-in replacement for langchain_openai.ChatOpenAI with MACAW protection.
32
+
33
+ Uses SecureOpenAI internally to route all LLM calls through MACAW's PEP.
34
+ This ensures policy enforcement, not just logging.
35
+
36
+ MACAW is completely invisible - use exactly like native LangChain.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ model: str = "gpt-4",
42
+ temperature: float = 0.7,
43
+ max_tokens: Optional[int] = None,
44
+ api_key: Optional[str] = None,
45
+ base_url: Optional[str] = None,
46
+ organization: Optional[str] = None,
47
+ timeout: Optional[float] = None,
48
+ max_retries: int = 2,
49
+ **kwargs
50
+ ):
51
+ """
52
+ Initialize SecureChatOpenAI.
53
+
54
+ Args:
55
+ model: OpenAI model name (e.g., "gpt-4", "gpt-3.5-turbo")
56
+ temperature: Sampling temperature (0.0 to 2.0)
57
+ max_tokens: Maximum tokens to generate
58
+ api_key: OpenAI API key (or use OPENAI_API_KEY env var)
59
+ base_url: Custom API base URL
60
+ organization: OpenAI organization ID
61
+ timeout: Request timeout in seconds
62
+ max_retries: Maximum retry attempts
63
+ **kwargs: Additional arguments
64
+ """
65
+ # Store configuration
66
+ self.model_name = model
67
+ self.temperature = temperature
68
+ self.max_tokens = max_tokens
69
+ self._kwargs = kwargs
70
+
71
+ # Store optional config for reference (not passed to SecureOpenAI)
72
+ self._base_url = base_url
73
+ self._organization = organization
74
+ self._timeout = timeout
75
+ self._max_retries = max_retries
76
+
77
+ # Create SecureOpenAI internally - it handles its own MACAWClient
78
+ # SecureOpenAI accepts: api_key, app_name, intent_policy
79
+ from macaw_adapters.openai import SecureOpenAI
80
+
81
+ self._secure_openai = SecureOpenAI(
82
+ app_name="langchain-openai",
83
+ api_key=api_key
84
+ )
85
+
86
+ # Track for cleanup
87
+ _instances.append(self)
88
+
89
+ logger.debug(f"[ChatOpenAI] Created with SecureOpenAI backend, model={model}")
90
+
91
+ def _to_openai_messages(self, input_data: Any) -> List[Dict[str, str]]:
92
+ """Convert LangChain input to OpenAI message format."""
93
+ if isinstance(input_data, str):
94
+ return [{"role": "user", "content": input_data}]
95
+
96
+ if isinstance(input_data, list):
97
+ messages = []
98
+ for msg in input_data:
99
+ if hasattr(msg, 'type') and hasattr(msg, 'content'):
100
+ # LangChain message object (HumanMessage, AIMessage, etc.)
101
+ role_map = {
102
+ "human": "user",
103
+ "ai": "assistant",
104
+ "system": "system",
105
+ "function": "function",
106
+ "tool": "tool"
107
+ }
108
+ role = role_map.get(msg.type, "user")
109
+ messages.append({"role": role, "content": str(msg.content)})
110
+ elif isinstance(msg, dict):
111
+ # Already in dict format
112
+ messages.append(msg)
113
+ else:
114
+ # Fallback: treat as user message
115
+ messages.append({"role": "user", "content": str(msg)})
116
+ return messages
117
+
118
+ # Fallback: single user message
119
+ return [{"role": "user", "content": str(input_data)}]
120
+
121
+ def _to_langchain_message(self, openai_response: Any) -> Any:
122
+ """Convert OpenAI ChatCompletion to LangChain AIMessage."""
123
+ try:
124
+ from langchain_core.messages import AIMessage
125
+
126
+ # Handle dict response
127
+ if isinstance(openai_response, dict):
128
+ choices = openai_response.get("choices", [])
129
+ if choices:
130
+ content = choices[0].get("message", {}).get("content", "")
131
+ return AIMessage(content=content)
132
+ return AIMessage(content="")
133
+
134
+ # Handle ChatCompletion object
135
+ if hasattr(openai_response, 'choices') and openai_response.choices:
136
+ content = openai_response.choices[0].message.content or ""
137
+ return AIMessage(content=content)
138
+
139
+ return AIMessage(content=str(openai_response))
140
+
141
+ except ImportError:
142
+ # langchain_core not available, return raw response
143
+ logger.warning("langchain_core not installed, returning raw response")
144
+ return openai_response
145
+
146
+ def _to_langchain_chunk(self, openai_chunk: Any) -> Any:
147
+ """Convert OpenAI streaming chunk to LangChain AIMessageChunk."""
148
+ try:
149
+ from langchain_core.messages import AIMessageChunk
150
+
151
+ # Handle dict chunk
152
+ if isinstance(openai_chunk, dict):
153
+ choices = openai_chunk.get("choices", [])
154
+ if choices:
155
+ delta = choices[0].get("delta", {})
156
+ content = delta.get("content", "")
157
+ return AIMessageChunk(content=content)
158
+ return AIMessageChunk(content="")
159
+
160
+ # Handle ChatCompletionChunk object
161
+ if hasattr(openai_chunk, 'choices') and openai_chunk.choices:
162
+ delta = openai_chunk.choices[0].delta
163
+ content = delta.content if delta and delta.content else ""
164
+ return AIMessageChunk(content=content)
165
+
166
+ return AIMessageChunk(content="")
167
+
168
+ except ImportError:
169
+ # Return raw chunk
170
+ return openai_chunk
171
+
172
+ def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
173
+ """
174
+ Invoke the LLM with MACAW protection.
175
+
176
+ Args:
177
+ input: The input prompt (string or list of messages)
178
+ config: Optional configuration dict
179
+ **kwargs: Additional arguments
180
+
181
+ Returns:
182
+ LLM response (AIMessage)
183
+ """
184
+ # Convert to OpenAI format
185
+ messages = self._to_openai_messages(input)
186
+
187
+ # Build params
188
+ params = {
189
+ "model": self.model_name,
190
+ "messages": messages,
191
+ "temperature": self.temperature,
192
+ }
193
+ if self.max_tokens:
194
+ params["max_tokens"] = self.max_tokens
195
+
196
+ # Merge additional kwargs
197
+ params.update(kwargs)
198
+
199
+ # Call SecureOpenAI (routes through PEP!)
200
+ response = self._secure_openai.chat.completions.create(**params)
201
+
202
+ # Convert to LangChain format
203
+ return self._to_langchain_message(response)
204
+
205
+ def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
206
+ """
207
+ Stream responses from the LLM with MACAW protection.
208
+
209
+ Args:
210
+ input: The input prompt
211
+ config: Optional configuration dict
212
+ **kwargs: Additional arguments
213
+
214
+ Yields:
215
+ Streaming chunks (AIMessageChunk)
216
+ """
217
+ # Convert to OpenAI format
218
+ messages = self._to_openai_messages(input)
219
+
220
+ # Build params
221
+ params = {
222
+ "model": self.model_name,
223
+ "messages": messages,
224
+ "temperature": self.temperature,
225
+ "stream": True,
226
+ }
227
+ if self.max_tokens:
228
+ params["max_tokens"] = self.max_tokens
229
+
230
+ params.update(kwargs)
231
+
232
+ # Stream from SecureOpenAI (routes through PEP!)
233
+ for chunk in self._secure_openai.chat.completions.create(**params):
234
+ yield self._to_langchain_chunk(chunk)
235
+
236
+ def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
237
+ """
238
+ Batch invoke the LLM with MACAW protection.
239
+
240
+ Args:
241
+ inputs: List of input prompts
242
+ config: Optional configuration dict
243
+ **kwargs: Additional arguments
244
+
245
+ Returns:
246
+ List of LLM responses (AIMessage)
247
+ """
248
+ # Process each input through invoke (each goes through PEP)
249
+ return [self.invoke(input, config=config, **kwargs) for input in inputs]
250
+
251
+ async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
252
+ """Async invoke with MACAW protection."""
253
+ # For now, delegate to sync version
254
+ # TODO: Add true async support when SecureOpenAI supports it
255
+ return self.invoke(input, config=config, **kwargs)
256
+
257
+ async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
258
+ """Async stream with MACAW protection."""
259
+ # Yield from sync stream
260
+ for chunk in self.stream(input, config=config, **kwargs):
261
+ yield chunk
262
+
263
+ async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
264
+ """Async batch with MACAW protection."""
265
+ return self.batch(inputs, config=config, **kwargs)
266
+
267
+ def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundChatOpenAI':
268
+ """
269
+ Bind this LLM to a specific user's identity.
270
+
271
+ Returns a BoundChatOpenAI that routes all calls through the user's
272
+ MACAWClient for per-user policy enforcement.
273
+
274
+ Args:
275
+ user_client: MACAWClient with user identity (from RemoteIdentityProvider)
276
+
277
+ Returns:
278
+ BoundChatOpenAI instance
279
+ """
280
+ return BoundChatOpenAI(self, user_client)
281
+
282
+ # Pass-through properties to match LangChain interface
283
+ @property
284
+ def model(self) -> str:
285
+ return self.model_name
286
+
287
+ def bind(self, **kwargs):
288
+ """Bind arguments to the LLM (LangChain compatibility)."""
289
+ # Create a new instance with merged kwargs
290
+ new_kwargs = {**self._kwargs, **kwargs}
291
+ return ChatOpenAI(
292
+ model=self.model_name,
293
+ temperature=self.temperature,
294
+ max_tokens=self.max_tokens,
295
+ **new_kwargs
296
+ )
297
+
298
+ def with_config(self, config: Dict):
299
+ """Return self with config (LangChain compatibility)."""
300
+ # Config is handled per-call, not stored
301
+ return self
302
+
303
+ def cleanup(self):
304
+ """Clean up resources."""
305
+ if hasattr(self, '_secure_openai') and self._secure_openai:
306
+ try:
307
+ self._secure_openai.cleanup()
308
+ except Exception as e:
309
+ logger.debug(f"Cleanup error (may be expected): {e}")
310
+
311
+
312
+ class BoundChatOpenAI:
313
+ """
314
+ Per-user wrapper for ChatOpenAI.
315
+
316
+ Created via ChatOpenAI.bind_to_user(user_client).
317
+ Routes all calls through the user's identity for per-user policy enforcement.
318
+ """
319
+
320
+ def __init__(self, parent: ChatOpenAI, user_client: 'MACAWClient'):
321
+ self._parent = parent
322
+ self._user_client = user_client
323
+
324
+ # Get bound SecureOpenAI for this user
325
+ self._bound_openai = parent._secure_openai.bind_to_user(user_client)
326
+
327
+ def _to_openai_messages(self, input_data: Any) -> List[Dict[str, str]]:
328
+ """Delegate to parent's conversion."""
329
+ return self._parent._to_openai_messages(input_data)
330
+
331
+ def _to_langchain_message(self, openai_response: Any) -> Any:
332
+ """Delegate to parent's conversion."""
333
+ return self._parent._to_langchain_message(openai_response)
334
+
335
+ def _to_langchain_chunk(self, openai_chunk: Any) -> Any:
336
+ """Delegate to parent's conversion."""
337
+ return self._parent._to_langchain_chunk(openai_chunk)
338
+
339
+ def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
340
+ """Invoke with user identity."""
341
+ messages = self._to_openai_messages(input)
342
+
343
+ params = {
344
+ "model": self._parent.model_name,
345
+ "messages": messages,
346
+ "temperature": self._parent.temperature,
347
+ }
348
+ if self._parent.max_tokens:
349
+ params["max_tokens"] = self._parent.max_tokens
350
+ params.update(kwargs)
351
+
352
+ # Call through bound SecureOpenAI (user identity!)
353
+ response = self._bound_openai.chat.completions.create(**params)
354
+ return self._to_langchain_message(response)
355
+
356
+ def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
357
+ """Stream with user identity."""
358
+ messages = self._to_openai_messages(input)
359
+
360
+ params = {
361
+ "model": self._parent.model_name,
362
+ "messages": messages,
363
+ "temperature": self._parent.temperature,
364
+ "stream": True,
365
+ }
366
+ if self._parent.max_tokens:
367
+ params["max_tokens"] = self._parent.max_tokens
368
+ params.update(kwargs)
369
+
370
+ for chunk in self._bound_openai.chat.completions.create(**params):
371
+ yield self._to_langchain_chunk(chunk)
372
+
373
+ def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
374
+ """Batch with user identity."""
375
+ return [self.invoke(input, config=config, **kwargs) for input in inputs]
376
+
377
+ async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
378
+ """Async invoke with user identity."""
379
+ return self.invoke(input, config=config, **kwargs)
380
+
381
+ async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
382
+ """Async stream with user identity."""
383
+ for chunk in self.stream(input, config=config, **kwargs):
384
+ yield chunk
385
+
386
+ async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
387
+ """Async batch with user identity."""
388
+ return self.batch(inputs, config=config, **kwargs)
389
+
390
+ @property
391
+ def model(self) -> str:
392
+ return self._parent.model_name
393
+
394
+ @property
395
+ def model_name(self) -> str:
396
+ return self._parent.model_name
397
+
398
+ @property
399
+ def temperature(self) -> float:
400
+ return self._parent.temperature
401
+
402
+
403
+ def cleanup():
404
+ """Clean up all ChatOpenAI instances."""
405
+ for instance in _instances:
406
+ try:
407
+ instance.cleanup()
408
+ except Exception as e:
409
+ logger.debug(f"Cleanup error: {e}")
410
+ _instances.clear()
@@ -0,0 +1,174 @@
1
+ """
2
+ MACAW Tool Wrappers - LangChain tools routed through MACAW PEP.
3
+
4
+ Usage:
5
+ from macaw_adapters.langchain.tools import SecureToolWrapper, wrap_tools
6
+
7
+ # Wrap individual tool
8
+ secure_tool = SecureToolWrapper(original_tool, macaw_client)
9
+
10
+ # Wrap list of tools
11
+ secure_tools = wrap_tools(tools, macaw_client)
12
+ """
13
+
14
+ import logging
15
+ from typing import Any, List, Optional
16
+
17
+ from langchain_core.tools import BaseTool
18
+ from langchain_core.callbacks import CallbackManagerForToolRun, AsyncCallbackManagerForToolRun
19
+ from macaw_client import MACAWClient
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class SecureToolWrapper(BaseTool):
25
+ """
26
+ Wrapper for LangChain tools that routes execution through MACAW PEP.
27
+
28
+ Inherits from BaseTool to be fully compatible with LangChain's
29
+ AgentExecutor and validation. Enforces security policies via MACAWClient.
30
+ """
31
+
32
+ # Pydantic fields
33
+ original_tool: Any = None
34
+ macaw_client: Any = None
35
+
36
+ class Config:
37
+ arbitrary_types_allowed = True
38
+
39
+ def __init__(self, original_tool: Any, macaw_client: MACAWClient, **kwargs):
40
+ """
41
+ Initialize secure tool wrapper.
42
+
43
+ Args:
44
+ original_tool: LangChain BaseTool instance
45
+ macaw_client: MACAWClient for policy enforcement
46
+ """
47
+ # Extract attributes from original tool for BaseTool
48
+ super().__init__(
49
+ name=original_tool.name,
50
+ description=original_tool.description,
51
+ original_tool=original_tool,
52
+ macaw_client=macaw_client,
53
+ **kwargs
54
+ )
55
+
56
+ # Copy optional attributes
57
+ if hasattr(original_tool, 'args_schema') and original_tool.args_schema:
58
+ self.args_schema = original_tool.args_schema
59
+ if hasattr(original_tool, 'return_direct'):
60
+ self.return_direct = original_tool.return_direct
61
+
62
+ # Register tool with MAPL-compliant name: tool:<app_name>/<tool_name>
63
+ app_name = getattr(macaw_client, 'app_name', 'langchain')
64
+ mapl_name = f"tool:{app_name}/{original_tool.name}"
65
+
66
+ def tool_handler(params):
67
+ """Handler that executes the original tool."""
68
+ tool_input = params.get("input", "")
69
+ return original_tool.run(tool_input)
70
+
71
+ macaw_client.register_tool(mapl_name, tool_handler)
72
+ logger.info(f"Registered secure tool: {original_tool.name} -> {mapl_name}")
73
+
74
+ def _run(
75
+ self,
76
+ tool_input: str = "",
77
+ run_manager: Optional[CallbackManagerForToolRun] = None,
78
+ **kwargs
79
+ ) -> str:
80
+ """Execute tool through MACAW PEP."""
81
+ # Use MAPL-compliant name: tool:<app_name>/<tool_name>
82
+ app_name = getattr(self.macaw_client, 'app_name', 'langchain')
83
+ mapl_name = f"tool:{app_name}/{self.name}"
84
+ logger.debug(f"[SecureTool] Routing {self.name} -> {mapl_name} through MACAW")
85
+
86
+ try:
87
+ # Build parameters dict
88
+ parameters = {"input": tool_input}
89
+
90
+ # Invoke tool through MACAW protocol with MAPL-compliant name
91
+ # Authenticated prompts are auto-created by invoke_tool() based on registry
92
+ result = self.macaw_client.invoke_tool(
93
+ tool_name=mapl_name,
94
+ parameters=parameters,
95
+ target_agent=self.macaw_client.agent_id
96
+ )
97
+ return str(result) if result is not None else ""
98
+
99
+ except Exception as e:
100
+ error_msg = str(e).lower()
101
+ if any(word in error_msg for word in ['denied', 'blocked', 'policy']):
102
+ logger.warning(f"MACAW blocked {mapl_name}: {e}")
103
+ return f"Access denied by security policy: {mapl_name}"
104
+ raise
105
+
106
+ async def _arun(
107
+ self,
108
+ tool_input: str = "",
109
+ run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
110
+ **kwargs
111
+ ) -> str:
112
+ """Async execution (delegates to sync)."""
113
+ return self._run(tool_input, **kwargs)
114
+
115
+
116
+ def wrap_tools(tools: List[Any], macaw_client: MACAWClient) -> List[SecureToolWrapper]:
117
+ """
118
+ Wrap a list of LangChain tools with MACAW security.
119
+
120
+ Args:
121
+ tools: List of LangChain BaseTool instances
122
+ macaw_client: MACAWClient for policy enforcement
123
+
124
+ Returns:
125
+ List of SecureToolWrapper instances
126
+ """
127
+ return [SecureToolWrapper(tool, macaw_client) for tool in tools]
128
+
129
+
130
+ def secure_tool(func=None, *, macaw_client: Optional[MACAWClient] = None):
131
+ """
132
+ Decorator to create a secure LangChain tool with MACAW protection.
133
+
134
+ Usage:
135
+ @secure_tool
136
+ def search_database(query: str) -> str:
137
+ '''Search the database for relevant information.'''
138
+ return database.search(query)
139
+
140
+ # Or with explicit client:
141
+ @secure_tool(macaw_client=my_client)
142
+ def my_tool(x: str) -> str:
143
+ '''My tool description.'''
144
+ return process(x)
145
+
146
+ Args:
147
+ func: The function to wrap (used when called without parentheses)
148
+ macaw_client: Optional MACAWClient for policy enforcement
149
+
150
+ Returns:
151
+ A SecureToolWrapper compatible with LangChain agents
152
+ """
153
+ from langchain_core.tools import tool as langchain_tool
154
+ from ._utils import get_or_create_client
155
+
156
+ def decorator(fn):
157
+ # Create LangChain tool from function
158
+ lc_tool = langchain_tool(fn)
159
+
160
+ # Get or create MACAW client
161
+ client = macaw_client or get_or_create_client("langchain-tools")
162
+ if client is None:
163
+ logger.warning("Could not create MACAWClient, returning unwrapped tool")
164
+ return lc_tool
165
+
166
+ # Wrap with MACAW security
167
+ return SecureToolWrapper(lc_tool, client)
168
+
169
+ if func is not None:
170
+ # Called without parentheses: @secure_tool
171
+ return decorator(func)
172
+ else:
173
+ # Called with parentheses: @secure_tool() or @secure_tool(macaw_client=...)
174
+ return decorator