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