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