@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,792 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SecureAnthropic - MACAW-protected Anthropic Claude wrapper.
4
+
5
+ Drop-in replacement for Anthropic client with MACAW security.
6
+ Uses MAPL-compliant tool names: tool:<app_name>/generate, tool:<app_name>/complete.
7
+
8
+ Supports three usage paths:
9
+ 1. Direct on service: client.messages.create() - simplest, app-level identity
10
+ 2. bind_to_user: service.bind_to_user(user_client) - per-user identity for SaaS
11
+ 3. invoke_tool: user.invoke_tool("tool:xxx/generate", ...) - explicit A2A control
12
+
13
+ Install with: pip install -e /path/to/secureAI
14
+ """
15
+
16
+ import os
17
+ import json
18
+ import logging
19
+ import inspect
20
+ from typing import Dict, Any, Optional, List
21
+
22
+ from anthropic import Anthropic
23
+ from anthropic.types import Message, ContentBlock, TextBlock, Usage
24
+ from macaw_client import MACAWClient
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class _StreamContextManager:
30
+ """
31
+ Context manager that provides Anthropic-compatible streaming interface.
32
+
33
+ Usage:
34
+ with client.messages.stream(...) as stream:
35
+ for text in stream.text_stream:
36
+ print(text, end="")
37
+ """
38
+
39
+ def __init__(self, iterator):
40
+ self._iterator = iterator
41
+ self._text_chunks = []
42
+
43
+ def __enter__(self):
44
+ return self
45
+
46
+ def __exit__(self, exc_type, exc_val, exc_tb):
47
+ # Clean up iterator if needed
48
+ pass
49
+
50
+ @property
51
+ def text_stream(self):
52
+ """Yield just the text content from streaming chunks."""
53
+ for chunk in self._iterator:
54
+ if isinstance(chunk, dict):
55
+ # Handle content_block_delta events
56
+ if chunk.get('type') == 'content_block_delta':
57
+ delta = chunk.get('delta', {})
58
+ text = delta.get('text', '')
59
+ if text:
60
+ yield text
61
+ elif hasattr(chunk, 'delta') and hasattr(chunk.delta, 'text'):
62
+ # Handle raw event objects
63
+ if chunk.delta.text:
64
+ yield chunk.delta.text
65
+
66
+ def __iter__(self):
67
+ """Iterate over raw chunks."""
68
+ return iter(self._iterator)
69
+
70
+
71
+ class SecureAnthropic:
72
+ """
73
+ Drop-in replacement for Anthropic Claude client with MACAW security.
74
+
75
+ Supports two modes:
76
+ 1. Service mode (default): Creates service agent, registers tools, handles Claude calls
77
+ 2. User mode (jwt_token provided): Creates user agent with identity for direct calls
78
+
79
+ For multi-user scenarios, use service mode + bind_to_user() to create per-user wrappers.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ api_key: str = None,
85
+ app_name: str = None,
86
+ intent_policy: Optional[Dict[str, Any]] = None,
87
+ # User mode parameters
88
+ jwt_token: str = None,
89
+ user_name: str = None
90
+ ):
91
+ """
92
+ Initialize SecureAnthropic wrapper.
93
+
94
+ Args:
95
+ api_key: Anthropic API key (or from env)
96
+ app_name: Application name for registration
97
+ intent_policy: Application-defined MACAW intent policy
98
+ jwt_token: If provided, creates user agent with this identity (user mode)
99
+ user_name: Optional user name for user mode
100
+ """
101
+ # Get API key
102
+ self.api_key = api_key or os.environ.get('ANTHROPIC_API_KEY')
103
+ if not self.api_key:
104
+ raise ValueError("Anthropic API key required")
105
+
106
+ # Real Claude client
107
+ self.claude_client = Anthropic(api_key=self.api_key)
108
+
109
+ # Application identity
110
+ self.app_name = app_name or "secure-claude-app"
111
+
112
+ # Determine mode based on jwt_token
113
+ self._mode = "user" if jwt_token else "service"
114
+ self._jwt_token = jwt_token
115
+ self._user_name = user_name
116
+
117
+ # User-registered tools (Claude can call these)
118
+ self.user_tools = {}
119
+
120
+ # Auto-discovered tools cache
121
+ self._discovered_tools = {}
122
+ self._tools_registered = False
123
+
124
+ # Application-provided intent policy (no defaults!)
125
+ self.intent_policy = intent_policy or {}
126
+
127
+ if self._mode == "service":
128
+ # SERVICE MODE: Register tools and handle Claude calls
129
+ # Tools with prompts declaration - MAPL-compliant: tool:<service>/<operation>
130
+ # Each tool declares which parameters are prompts for automatic authentication
131
+ self.tools = {
132
+ f"tool:{self.app_name}/generate": {
133
+ "handler": self._handle_generate,
134
+ "prompts": ["messages"] # messages param is a prompt
135
+ },
136
+ f"tool:{self.app_name}/complete": {
137
+ "handler": self._handle_complete,
138
+ "prompts": ["prompt"] # prompt param is a prompt
139
+ }
140
+ }
141
+
142
+ # Create MACAWClient with unified tools config
143
+ self.macaw_client = MACAWClient(
144
+ app_name=self.app_name,
145
+ app_version="1.0.0",
146
+ intent_policy=self.intent_policy,
147
+ tools=self.tools # Unified: {name: {handler, prompts, ...}}
148
+ )
149
+ else:
150
+ # USER MODE: Create user agent with identity
151
+ self.tools = {}
152
+
153
+ self.macaw_client = MACAWClient(
154
+ user_name=user_name,
155
+ iam_token=jwt_token,
156
+ agent_type="user",
157
+ app_name=self.app_name,
158
+ app_version="1.0.0",
159
+ intent_policy=self.intent_policy or {
160
+ "purpose": f"Claude access for {user_name or 'user'}"
161
+ }
162
+ )
163
+
164
+ # Register with LocalAgent and get server ID
165
+ if self.macaw_client.register():
166
+ self.server_id = self.macaw_client.agent_id
167
+ logger.info(f"SecureAnthropic registered as: {self.server_id} (mode: {self._mode})")
168
+ else:
169
+ raise RuntimeError("Failed to register with MACAW LocalAgent")
170
+
171
+ # Mimic Anthropic API structure
172
+ # MACAW-protected namespaces (route through PEP)
173
+ self.messages = self._MessagesNamespace(self)
174
+ self.completions = self._CompletionsNamespace(self)
175
+
176
+ # =========================================================================
177
+ # Pass-through properties for non-MACAW-protected APIs
178
+ # These delegate directly to the underlying Anthropic client
179
+ # =========================================================================
180
+
181
+ @property
182
+ def beta(self):
183
+ """
184
+ Pass-through to Anthropic beta API (message batches).
185
+
186
+ Not MACAW-protected - batch operations use service's API key directly.
187
+ """
188
+ return self.claude_client.beta
189
+
190
+ def count_tokens(self, *args, **kwargs):
191
+ """
192
+ Pass-through to Anthropic count_tokens method.
193
+
194
+ Not MACAW-protected - token counting uses service's API key directly.
195
+ """
196
+ return self.claude_client.count_tokens(*args, **kwargs)
197
+
198
+ def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundSecureAnthropic':
199
+ """
200
+ Bind this SecureAnthropic service to a user's MACAW client.
201
+
202
+ Creates a lightweight wrapper that routes calls through the user's
203
+ client to this service, enabling per-user identity and policy enforcement.
204
+
205
+ Only valid in service mode.
206
+
207
+ Security validations:
208
+ - Service must be in service mode
209
+ - User client must be a valid MACAWClient instance
210
+ - User client must be registered with LocalAgent
211
+ - User client should be a "user" type agent
212
+
213
+ Args:
214
+ user_client: A registered MACAWClient with user identity
215
+
216
+ Returns:
217
+ BoundSecureAnthropic wrapper for this user
218
+
219
+ Raises:
220
+ ValueError: If service not in service mode or client validation fails
221
+ """
222
+ if self._mode != "service":
223
+ raise ValueError("bind_to_user() only valid for service-mode SecureAnthropic")
224
+
225
+ # Validate user_client is a MACAWClient instance
226
+ if not hasattr(user_client, 'agent_id') or not hasattr(user_client, 'invoke_tool'):
227
+ raise ValueError("bind_to_user() requires a valid MACAWClient instance")
228
+
229
+ # Validate user_client is registered
230
+ if not getattr(user_client, 'registered', False):
231
+ raise ValueError("bind_to_user() requires a registered MACAWClient. Call register() first.")
232
+
233
+ # Validate agent_type is "user" (warning only, not blocking)
234
+ agent_type = getattr(user_client, 'agent_type', None)
235
+ if agent_type and agent_type != "user":
236
+ logger.warning(f"bind_to_user() called with agent_type='{agent_type}' (expected 'user'). "
237
+ f"User identity and policy enforcement may not work as expected.")
238
+
239
+ return BoundSecureAnthropic(self, user_client)
240
+
241
+ def register_tool(self, name: str, handler: callable) -> 'SecureAnthropic':
242
+ """
243
+ Register a tool that Claude can call.
244
+
245
+ Args:
246
+ name: Tool name (must match Claude function name)
247
+ handler: Function to handle tool execution
248
+
249
+ Returns:
250
+ Self for chaining
251
+ """
252
+ # Store with MAPL-compliant name for _handle_generate() lookup
253
+ mapl_name = f"tool:{self.app_name}/{name}"
254
+
255
+ # Wrap handler to accept params dict and unpack as kwargs
256
+ def wrapped_handler(params):
257
+ return handler(**params)
258
+
259
+ self.user_tools[mapl_name] = wrapped_handler
260
+
261
+ # Update MACAWClient with new tool using MAPL name (for invoke_tool routing)
262
+ self.macaw_client.register_tool(mapl_name, wrapped_handler)
263
+
264
+ logger.info(f"Registered tool: {name}")
265
+ return self
266
+
267
+ def _auto_discover_tools(self, tools_metadata):
268
+ """
269
+ Auto-discover tool implementations from caller's context.
270
+
271
+ Args:
272
+ tools_metadata: List of tool definitions from Claude call
273
+ """
274
+ if self._tools_registered:
275
+ return # Already discovered
276
+
277
+ # Get caller's frame (need to go back through the call stack)
278
+ frame = inspect.currentframe()
279
+ caller_frame = frame.f_back.f_back.f_back
280
+ if not caller_frame:
281
+ logger.warning("Could not access caller frame for auto-discovery")
282
+ return
283
+
284
+ caller_globals = caller_frame.f_globals
285
+ caller_locals = caller_frame.f_locals
286
+
287
+ # Combine locals and globals for search
288
+ search_scope = {**caller_globals, **caller_locals}
289
+
290
+ # Discover implementations for each tool
291
+ for tool_def in tools_metadata:
292
+ func_name = tool_def.get("name")
293
+
294
+ # Use MAPL-compliant name: tool:<app_name>/<func_name>
295
+ mapl_name = f"tool:{self.app_name}/{func_name}"
296
+ if func_name and mapl_name not in self.user_tools:
297
+ # Look for function in caller's scope
298
+ if func_name in search_scope:
299
+ func = search_scope[func_name]
300
+ if callable(func):
301
+ logger.info(f"Auto-discovered tool: {func_name} -> {mapl_name}")
302
+ self._discovered_tools[mapl_name] = func
303
+ self.user_tools[mapl_name] = func
304
+ else:
305
+ logger.warning(f"Tool '{func_name}' not found in caller's scope")
306
+
307
+ # Sync all discovered tools with MACAW agent using public API
308
+ if self._discovered_tools:
309
+ # Update MACAWClient with all new tools
310
+ for mapl_name, func in self._discovered_tools.items():
311
+ # Create wrapper handler that accepts dict parameter
312
+ def create_handler(tool_func):
313
+ def handler(params):
314
+ return tool_func(**params)
315
+ return handler
316
+ self.macaw_client.register_tool(mapl_name, create_handler(func))
317
+ self._tools_registered = True
318
+ logger.info(f"Auto-registered {len(self._discovered_tools)} tools with MACAW")
319
+
320
+ def _handle_generate(self, params: Dict[str, Any]) -> Dict[str, Any]:
321
+ """
322
+ Handle 'generate' tool (messages creation).
323
+
324
+ This runs INSIDE ToolAgent, so it's already PEP-protected!
325
+ Returns a serializable dict for MACAW protocol, or an iterator for streaming.
326
+ """
327
+ try:
328
+ # Check if streaming is requested
329
+ is_streaming = params.get('stream', False)
330
+
331
+ if is_streaming:
332
+ # Streaming mode: return iterator
333
+ return self._stream_generate(params)
334
+
335
+ # Non-streaming mode: existing behavior
336
+ # Call real Claude API (params are clean - no MACAW internals)
337
+ response = self.claude_client.messages.create(**params)
338
+
339
+ # Check if Claude wants to call tools
340
+ if hasattr(response, 'content') and response.content:
341
+ for content_block in response.content:
342
+ if content_block.type == 'tool_use':
343
+ func_name = content_block.name
344
+ tool_args = content_block.input
345
+ # Convert to MAPL-compliant name: tool:<app_name>/<func_name>
346
+ mapl_name = f"tool:{self.app_name}/{func_name}"
347
+ logger.info(f"Claude requested tool use: {func_name} -> {mapl_name}")
348
+
349
+ # Check if we have this tool (using MAPL name)
350
+ if mapl_name not in self.user_tools:
351
+ result = {
352
+ 'error': f"Tool not found: {mapl_name}",
353
+ 'available_tools': list(self.user_tools.keys())
354
+ }
355
+ else:
356
+ # Invoke tool through MACAW (goes through PEP!)
357
+ try:
358
+ result = self.macaw_client.invoke_tool(
359
+ tool_name=mapl_name,
360
+ parameters=tool_args,
361
+ target_agent=self.server_id # Route to ourselves!
362
+ )
363
+ logger.info(f"Tool {mapl_name} executed successfully")
364
+ except Exception as e:
365
+ error_msg = str(e)
366
+ # Check if MACAW blocked it
367
+ if 'denied' in error_msg.lower() or 'policy' in error_msg.lower():
368
+ logger.warning(f"MACAW blocked tool {mapl_name}: {error_msg}")
369
+ result = {
370
+ 'error': 'Access denied by security policy',
371
+ 'tool': mapl_name,
372
+ 'reason': 'Policy violation'
373
+ }
374
+ else:
375
+ result = {'error': str(e)}
376
+
377
+ # Build messages for continuation
378
+ messages = list(params.get('messages', []))
379
+
380
+ # Add assistant message with tool use (use original func_name for Claude)
381
+ messages.append({
382
+ "role": "assistant",
383
+ "content": [{"type": "tool_use", "id": content_block.id,
384
+ "name": func_name, "input": tool_args}]
385
+ })
386
+
387
+ # Add tool result
388
+ messages.append({
389
+ "role": "user",
390
+ "content": [{
391
+ "type": "tool_result",
392
+ "tool_use_id": content_block.id,
393
+ "content": json.dumps(result)
394
+ }]
395
+ })
396
+
397
+ # Continue conversation with tool results
398
+ final_params = params.copy()
399
+ final_params['messages'] = messages
400
+
401
+ # Remove tools to prevent infinite recursion
402
+ if 'tools' in final_params:
403
+ del final_params['tools']
404
+
405
+ final_response = self.claude_client.messages.create(**final_params)
406
+
407
+ # Convert to serializable dict for MACAW protocol
408
+ return self._response_to_dict(final_response)
409
+
410
+ # No tool calls, convert response to dict
411
+ return self._response_to_dict(response)
412
+
413
+ except Exception as e:
414
+ logger.error(f"Error in generate handler: {e}")
415
+ return {'error': str(e)}
416
+
417
+ def _stream_generate(self, params: Dict[str, Any]):
418
+ """
419
+ Handle streaming message creation.
420
+
421
+ Returns an iterator that yields chunks from the Anthropic streaming API.
422
+ Each chunk is converted to a serializable dict.
423
+ """
424
+ try:
425
+ # Call Anthropic with streaming
426
+ with self.claude_client.messages.stream(**{k: v for k, v in params.items() if k != 'stream'}) as stream:
427
+ for event in stream:
428
+ # Convert event to serializable dict
429
+ if hasattr(event, 'type'):
430
+ chunk_dict = {
431
+ 'type': event.type,
432
+ }
433
+ # Handle different event types
434
+ if event.type == 'content_block_delta':
435
+ if hasattr(event, 'delta') and hasattr(event.delta, 'text'):
436
+ chunk_dict['delta'] = {'text': event.delta.text}
437
+ if hasattr(event, 'index'):
438
+ chunk_dict['index'] = event.index
439
+ elif event.type == 'message_start':
440
+ if hasattr(event, 'message'):
441
+ chunk_dict['message'] = {
442
+ 'id': event.message.id,
443
+ 'model': event.message.model,
444
+ 'role': event.message.role,
445
+ }
446
+ elif event.type == 'message_delta':
447
+ if hasattr(event, 'delta'):
448
+ chunk_dict['delta'] = {}
449
+ if hasattr(event.delta, 'stop_reason'):
450
+ chunk_dict['delta']['stop_reason'] = event.delta.stop_reason
451
+ if hasattr(event, 'usage'):
452
+ chunk_dict['usage'] = {
453
+ 'output_tokens': event.usage.output_tokens
454
+ }
455
+ elif event.type == 'message_stop':
456
+ pass # Just mark end of stream
457
+
458
+ yield chunk_dict
459
+
460
+ except Exception as e:
461
+ logger.error(f"Error in streaming generate: {e}")
462
+ # Yield error as final chunk
463
+ yield {'error': str(e)}
464
+
465
+ def _handle_complete(self, params: Dict[str, Any]) -> Dict[str, Any]:
466
+ """
467
+ Handle 'complete' tool (legacy completions).
468
+
469
+ Returns a serializable dict for MACAW protocol.
470
+ """
471
+ try:
472
+ response = self.claude_client.completions.create(**params)
473
+ return {
474
+ 'id': getattr(response, 'id', None),
475
+ 'type': 'completion',
476
+ 'completion': response.completion,
477
+ 'model': getattr(response, 'model', None),
478
+ 'stop_reason': getattr(response, 'stop_reason', None)
479
+ }
480
+ except Exception as e:
481
+ logger.error(f"Error in complete handler: {e}")
482
+ return {'error': str(e)}
483
+
484
+ def _response_to_dict(self, response) -> Dict[str, Any]:
485
+ """Convert Anthropic Message response to serializable dict."""
486
+ result = {
487
+ 'id': response.id,
488
+ 'type': response.type,
489
+ 'role': response.role,
490
+ 'model': response.model,
491
+ 'content': [],
492
+ 'stop_reason': response.stop_reason,
493
+ 'stop_sequence': response.stop_sequence,
494
+ 'usage': {
495
+ 'input_tokens': response.usage.input_tokens,
496
+ 'output_tokens': response.usage.output_tokens
497
+ } if response.usage else None
498
+ }
499
+
500
+ # Convert content blocks
501
+ for block in response.content:
502
+ if hasattr(block, 'text'):
503
+ result['content'].append({
504
+ 'type': 'text',
505
+ 'text': block.text
506
+ })
507
+ elif hasattr(block, 'type') and block.type == 'tool_use':
508
+ result['content'].append({
509
+ 'type': 'tool_use',
510
+ 'id': block.id,
511
+ 'name': block.name,
512
+ 'input': block.input
513
+ })
514
+
515
+ return result
516
+
517
+ def _dict_to_message(self, data: Dict[str, Any]) -> Message:
518
+ """Reconstruct Anthropic Message from dict."""
519
+ # Build content blocks
520
+ content = []
521
+ for block_data in data.get('content', []):
522
+ if block_data.get('type') == 'text':
523
+ content.append(TextBlock(type='text', text=block_data['text']))
524
+ # Add other block types as needed
525
+
526
+ # Build usage
527
+ usage = None
528
+ if data.get('usage'):
529
+ usage = Usage(
530
+ input_tokens=data['usage']['input_tokens'],
531
+ output_tokens=data['usage']['output_tokens']
532
+ )
533
+
534
+ return Message(
535
+ id=data['id'],
536
+ type=data.get('type', 'message'),
537
+ role=data.get('role', 'assistant'),
538
+ model=data['model'],
539
+ content=content,
540
+ stop_reason=data.get('stop_reason'),
541
+ stop_sequence=data.get('stop_sequence'),
542
+ usage=usage
543
+ )
544
+
545
+ # Namespace classes to mimic Anthropic API structure
546
+ class _MessagesNamespace:
547
+ def __init__(self, parent):
548
+ self.parent = parent
549
+
550
+ def create(self, **kwargs):
551
+ """
552
+ Create message - routes through MACAW.
553
+
554
+ This mimics Anthropic's API but goes through PEP!
555
+ Adapter creates authenticated prompts based on its own prompts declaration.
556
+ Supports streaming when stream=True is passed.
557
+ """
558
+ # Auto-discover tools if provided
559
+ tools = kwargs.get('tools')
560
+ if tools:
561
+ self.parent._auto_discover_tools(tools)
562
+
563
+ tool_name = f"tool:{self.parent.app_name}/generate"
564
+ is_streaming = kwargs.get('stream', False)
565
+
566
+ # Route through MACAW's generate resource
567
+ # MAPL-compliant: tool:<service>/generate
568
+ # Authenticated prompts are auto-created by invoke_tool() based on registry
569
+ result = self.parent.macaw_client.invoke_tool(
570
+ tool_name=tool_name,
571
+ parameters=kwargs,
572
+ target_agent=self.parent.server_id, # Route to ourselves!
573
+ stream=is_streaming # Explicit streaming flag
574
+ )
575
+
576
+ if is_streaming:
577
+ # Streaming mode: result is a StreamingResultIterator, wrap in Anthropic types
578
+ return self._wrap_streaming_response(result)
579
+
580
+ # Handle errors
581
+ if isinstance(result, dict) and 'error' in result:
582
+ raise Exception(f"MACAW error: {result['error']}")
583
+
584
+ # Reconstruct Message from dict
585
+ return self.parent._dict_to_message(result)
586
+
587
+ def _wrap_streaming_response(self, iterator):
588
+ """Wrap streaming chunks for Anthropic-compatible iteration."""
589
+ for chunk in iterator:
590
+ if isinstance(chunk, dict):
591
+ if 'error' in chunk:
592
+ raise Exception(f"MACAW streaming error: {chunk['error']}")
593
+ yield chunk
594
+ else:
595
+ yield chunk
596
+
597
+ def stream(self, **kwargs):
598
+ """
599
+ Stream message creation - context manager style.
600
+
601
+ Usage:
602
+ with client.messages.stream(...) as stream:
603
+ for text in stream.text_stream:
604
+ print(text, end="")
605
+ """
606
+ # Use create with stream=True, wrap in context manager
607
+ kwargs['stream'] = True
608
+ return _StreamContextManager(self.create(**kwargs))
609
+
610
+ class _CompletionsNamespace:
611
+ def __init__(self, parent):
612
+ self.parent = parent
613
+
614
+ def create(self, **kwargs):
615
+ """Create completion - routes through MACAW."""
616
+ tool_name = f"tool:{self.parent.app_name}/complete"
617
+
618
+ # MAPL-compliant: tool:<service>/complete
619
+ # Authenticated prompts are auto-created by invoke_tool() based on registry
620
+ result = self.parent.macaw_client.invoke_tool(
621
+ tool_name=tool_name,
622
+ parameters=kwargs,
623
+ target_agent=self.parent.server_id # Route to ourselves!
624
+ )
625
+
626
+ # Handle errors
627
+ if isinstance(result, dict) and 'error' in result:
628
+ raise Exception(f"MACAW error: {result['error']}")
629
+
630
+ # Return raw completion dict (legacy API)
631
+ return result
632
+
633
+
634
+ class BoundSecureAnthropic:
635
+ """
636
+ Per-user wrapper for SecureAnthropic service.
637
+
638
+ Created via SecureAnthropic.bind_to_user(user_client).
639
+ Routes all calls through the user's MACAWClient to the service,
640
+ enabling per-user identity and policy enforcement.
641
+
642
+ Authenticated prompts are auto-created by invoke_tool based on
643
+ the service's prompts declaration in the registry.
644
+
645
+ Call unbind() to invalidate this binding when done.
646
+ """
647
+
648
+ def __init__(self, service: SecureAnthropic, user_client: 'MACAWClient'):
649
+ """
650
+ Initialize bound wrapper.
651
+
652
+ Args:
653
+ service: The shared SecureAnthropic service (must be in service mode)
654
+ user_client: User's registered MACAWClient with identity
655
+ """
656
+ self._service = service
657
+ self._user_client = user_client
658
+ self._bound = True
659
+
660
+ # Create Anthropic-compatible API namespaces
661
+ self.messages = self._MessagesNamespace(self)
662
+ self.completions = self._CompletionsNamespace(self)
663
+
664
+ @property
665
+ def service(self) -> SecureAnthropic:
666
+ """Get the bound service (raises if unbound)."""
667
+ if not self._bound:
668
+ raise RuntimeError("BoundSecureAnthropic has been unbound. Create a new binding with bind_to_user().")
669
+ return self._service
670
+
671
+ @property
672
+ def user_client(self) -> 'MACAWClient':
673
+ """Get the bound user client (raises if unbound)."""
674
+ if not self._bound:
675
+ raise RuntimeError("BoundSecureAnthropic has been unbound. Create a new binding with bind_to_user().")
676
+ return self._user_client
677
+
678
+ def unbind(self):
679
+ """
680
+ Unbind this wrapper, invalidating all future calls.
681
+
682
+ After unbinding:
683
+ - All API calls will raise RuntimeError
684
+ - References to service and user_client are cleared
685
+ - A new binding must be created via bind_to_user()
686
+
687
+ This is useful for cleanup and ensuring bindings are not reused
688
+ after a user session ends.
689
+ """
690
+ if not self._bound:
691
+ logger.warning("BoundSecureAnthropic.unbind() called on already unbound instance")
692
+ return
693
+
694
+ logger.info(f"Unbinding SecureAnthropic from user {self._user_client.agent_id}")
695
+ self._bound = False
696
+ self._service = None
697
+ self._user_client = None
698
+
699
+ @property
700
+ def is_bound(self) -> bool:
701
+ """Check if this wrapper is still bound."""
702
+ return self._bound
703
+
704
+ # =========================================================================
705
+ # Pass-through properties for non-MACAW-protected APIs
706
+ # These delegate to the service's underlying Anthropic client
707
+ # =========================================================================
708
+
709
+ @property
710
+ def beta(self):
711
+ """Pass-through to Anthropic beta API (message batches) via service."""
712
+ return self.service.claude_client.beta
713
+
714
+ def count_tokens(self, *args, **kwargs):
715
+ """Pass-through to Anthropic count_tokens method via service."""
716
+ return self.service.claude_client.count_tokens(*args, **kwargs)
717
+
718
+ class _MessagesNamespace:
719
+ def __init__(self, bound: 'BoundSecureAnthropic'):
720
+ self.bound = bound
721
+
722
+ def create(self, **kwargs):
723
+ """
724
+ Create message via user's client -> service.
725
+
726
+ Authenticated prompts are auto-created by invoke_tool.
727
+ Supports streaming with stream=True parameter.
728
+ """
729
+ tool_name = f"tool:{self.bound.service.app_name}/generate"
730
+ is_streaming = kwargs.get('stream', False)
731
+
732
+ # Route through user's client (auto-creates auth prompts!)
733
+ result = self.bound.user_client.invoke_tool(
734
+ tool_name=tool_name,
735
+ parameters=kwargs,
736
+ target_agent=self.bound.service.server_id,
737
+ stream=is_streaming # Explicit streaming flag
738
+ )
739
+
740
+ if is_streaming:
741
+ # Streaming mode: result is a StreamingResultIterator, wrap for iteration
742
+ return self._wrap_streaming_response(result)
743
+
744
+ # Handle errors
745
+ if isinstance(result, dict) and 'error' in result:
746
+ raise Exception(f"MACAW error: {result['error']}")
747
+
748
+ # Reconstruct Message from dict
749
+ return self.bound.service._dict_to_message(result)
750
+
751
+ def _wrap_streaming_response(self, iterator):
752
+ """Wrap streaming chunks for Anthropic-compatible iteration."""
753
+ for chunk in iterator:
754
+ if isinstance(chunk, dict):
755
+ if 'error' in chunk:
756
+ raise Exception(f"MACAW streaming error: {chunk['error']}")
757
+ yield chunk
758
+ else:
759
+ yield chunk
760
+
761
+ def stream(self, **kwargs):
762
+ """
763
+ Stream message creation - context manager style.
764
+
765
+ Usage:
766
+ with user_client.messages.stream(...) as stream:
767
+ for text in stream.text_stream:
768
+ print(text, end="")
769
+ """
770
+ # Use create with stream=True, wrap in context manager
771
+ kwargs['stream'] = True
772
+ return _StreamContextManager(self.create(**kwargs))
773
+
774
+ class _CompletionsNamespace:
775
+ def __init__(self, bound: 'BoundSecureAnthropic'):
776
+ self.bound = bound
777
+
778
+ def create(self, **kwargs):
779
+ """Create completion via user's client -> service."""
780
+ tool_name = f"tool:{self.bound.service.app_name}/complete"
781
+
782
+ result = self.bound.user_client.invoke_tool(
783
+ tool_name=tool_name,
784
+ parameters=kwargs,
785
+ target_agent=self.bound.service.server_id
786
+ )
787
+
788
+ if isinstance(result, dict) and 'error' in result:
789
+ raise Exception(f"MACAW error: {result['error']}")
790
+
791
+ # Return raw completion dict (legacy API)
792
+ return result