@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.
- package/LICENSE.md +339 -0
- package/bin/install.js +422 -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,231 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
1b_grant_researcher.py - Reusable Capability Grant (Researcher)
|
|
4
|
+
|
|
5
|
+
A researcher needs admin approval to use web_search.
|
|
6
|
+
- First invocation: BLOCKS until admin approves
|
|
7
|
+
- Second+ invocations: IMMEDIATE (reuses existing approval)
|
|
8
|
+
|
|
9
|
+
The key difference from Example 1a is: one_time=False
|
|
10
|
+
|
|
11
|
+
This demonstrates:
|
|
12
|
+
- Reusable grants: approve once, use many times
|
|
13
|
+
- time_to_live: how long the grant lasts
|
|
14
|
+
- attestation_accessed audit event on reuse
|
|
15
|
+
|
|
16
|
+
NOTE: This example requires interactive approval and is not suitable
|
|
17
|
+
for automated test harnesses.
|
|
18
|
+
|
|
19
|
+
Prerequisites:
|
|
20
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
21
|
+
- Identity Provider configured (Console -> Settings -> Identity Bridge)
|
|
22
|
+
- Test users: alice/Alice123!, bob/Bob@123! (bob needs admin role)
|
|
23
|
+
|
|
24
|
+
Run:
|
|
25
|
+
# Terminal 1: Run researcher request (will block waiting)
|
|
26
|
+
python 1b_grant_researcher.py
|
|
27
|
+
|
|
28
|
+
# Terminal 2: Run admin approval
|
|
29
|
+
python 1b_grant_admin.py
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
from macaw_client import MACAWClient, RemoteIdentityProvider
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def web_search_handler(params):
|
|
39
|
+
"""Simulate web search."""
|
|
40
|
+
query = params.get('query', '')
|
|
41
|
+
print(f"\n [TOOL] web_search invoked!")
|
|
42
|
+
print(f" [TOOL] Query: {query}")
|
|
43
|
+
|
|
44
|
+
# Simulate search results
|
|
45
|
+
result = {
|
|
46
|
+
"status": "success",
|
|
47
|
+
"query": query,
|
|
48
|
+
"results": [
|
|
49
|
+
{"title": f"Result 1 for '{query}'", "url": "https://example.com/1"},
|
|
50
|
+
{"title": f"Result 2 for '{query}'", "url": "https://example.com/2"},
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
print(f" [TOOL] Returning {len(result['results'])} results")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
print("=" * 60)
|
|
59
|
+
print("Example 1b: Reusable Capability Grant - Researcher")
|
|
60
|
+
print("=" * 60)
|
|
61
|
+
|
|
62
|
+
# Step 1: Create Search Service
|
|
63
|
+
print("\n[Step 1] Creating Search Service...")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
search_service = MACAWClient(
|
|
67
|
+
app_name="search",
|
|
68
|
+
agent_type="service",
|
|
69
|
+
tools={
|
|
70
|
+
"tool:search/web_search": {
|
|
71
|
+
"handler": web_search_handler,
|
|
72
|
+
"description": "Search the web for information"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not search_service.register():
|
|
78
|
+
print(" ERROR: Failed to register Search Service")
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
print(f" Service ID: {search_service.agent_id}")
|
|
82
|
+
print(f" Provides: tool:search/web_search")
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f" ERROR: {e}")
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
# Step 2: Authenticate as researcher (using alice credentials)
|
|
89
|
+
print("\n[Step 2] Authenticating as researcher...")
|
|
90
|
+
try:
|
|
91
|
+
jwt_token, _ = RemoteIdentityProvider().login("alice", "Alice123!")
|
|
92
|
+
print(" Got JWT token")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f" ERROR: Failed to authenticate: {e}")
|
|
95
|
+
search_service.unregister()
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
# Step 3: Create researcher agent with REUSABLE attestation (one_time=False)
|
|
99
|
+
print("\n[Step 3] Creating researcher agent...")
|
|
100
|
+
try:
|
|
101
|
+
researcher = MACAWClient(
|
|
102
|
+
user_name="researcher",
|
|
103
|
+
iam_token=jwt_token,
|
|
104
|
+
agent_type="user",
|
|
105
|
+
app_name="research-app",
|
|
106
|
+
intent_policy={
|
|
107
|
+
"resources": ["tool:search/web_search"],
|
|
108
|
+
# Attestation always required for this tool
|
|
109
|
+
"attestations": ["capability_web_search"],
|
|
110
|
+
# Attestation metadata - REUSABLE
|
|
111
|
+
"constraints": {
|
|
112
|
+
"attestations": {
|
|
113
|
+
"capability_web_search": {
|
|
114
|
+
"approval_criteria": "role:admin",
|
|
115
|
+
"timeout": 300, # 5 min to approve
|
|
116
|
+
"one_time": False, # KEY: Reusable!
|
|
117
|
+
"time_to_live": 3600 # Valid for 1 hour (or None for forever)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if not researcher.register():
|
|
125
|
+
print(" ERROR: Failed to register researcher agent")
|
|
126
|
+
search_service.unregister()
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
print(f" Agent ID: {researcher.agent_id}")
|
|
130
|
+
print(f" one_time=False (REUSABLE capability)")
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f" ERROR: {e}")
|
|
134
|
+
search_service.unregister()
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
# Step 4: First invocation - should BLOCK for approval
|
|
138
|
+
print("\n" + "=" * 60)
|
|
139
|
+
print("FIRST INVOCATION - Will block for admin approval")
|
|
140
|
+
print("=" * 60)
|
|
141
|
+
print("\n >>> Run 1b_grant_admin.py in another terminal <<<")
|
|
142
|
+
print("-" * 60)
|
|
143
|
+
|
|
144
|
+
start_time = time.time()
|
|
145
|
+
try:
|
|
146
|
+
result = researcher.invoke_tool(
|
|
147
|
+
tool_name="tool:search/web_search",
|
|
148
|
+
target_agent=search_service.agent_id,
|
|
149
|
+
parameters={"query": "MACAW security framework"}
|
|
150
|
+
)
|
|
151
|
+
elapsed = time.time() - start_time
|
|
152
|
+
print(f"\n FIRST SEARCH COMPLETED! (took {elapsed:.1f}s)")
|
|
153
|
+
print(f" Result: {json.dumps(result, indent=2)}")
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"\n ERROR on first invocation: {e}")
|
|
157
|
+
researcher.unregister()
|
|
158
|
+
search_service.unregister()
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
# Step 5: Second invocation - should be IMMEDIATE (no blocking!)
|
|
162
|
+
print("\n" + "=" * 60)
|
|
163
|
+
print("SECOND INVOCATION - Should be IMMEDIATE (reusing approval)")
|
|
164
|
+
print("=" * 60)
|
|
165
|
+
|
|
166
|
+
start_time = time.time()
|
|
167
|
+
try:
|
|
168
|
+
result = researcher.invoke_tool(
|
|
169
|
+
tool_name="tool:search/web_search",
|
|
170
|
+
target_agent=search_service.agent_id,
|
|
171
|
+
parameters={"query": "AI agent governance"}
|
|
172
|
+
)
|
|
173
|
+
elapsed = time.time() - start_time
|
|
174
|
+
print(f"\n SECOND SEARCH COMPLETED! (took {elapsed:.1f}s)")
|
|
175
|
+
if elapsed < 2.0:
|
|
176
|
+
print(" *** REUSED EXISTING APPROVAL - NO BLOCKING! ***")
|
|
177
|
+
print(f" Result: {json.dumps(result, indent=2)}")
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"\n ERROR on second invocation: {e}")
|
|
181
|
+
|
|
182
|
+
# Step 6: Third invocation - also immediate
|
|
183
|
+
print("\n" + "=" * 60)
|
|
184
|
+
print("THIRD INVOCATION - Also immediate")
|
|
185
|
+
print("=" * 60)
|
|
186
|
+
|
|
187
|
+
start_time = time.time()
|
|
188
|
+
try:
|
|
189
|
+
result = researcher.invoke_tool(
|
|
190
|
+
tool_name="tool:search/web_search",
|
|
191
|
+
target_agent=search_service.agent_id,
|
|
192
|
+
parameters={"query": "enterprise AI security"}
|
|
193
|
+
)
|
|
194
|
+
elapsed = time.time() - start_time
|
|
195
|
+
print(f"\n THIRD SEARCH COMPLETED! (took {elapsed:.1f}s)")
|
|
196
|
+
if elapsed < 2.0:
|
|
197
|
+
print(" *** REUSED EXISTING APPROVAL - NO BLOCKING! ***")
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"\n ERROR on third invocation: {e}")
|
|
201
|
+
|
|
202
|
+
# Cleanup
|
|
203
|
+
print("\n[Cleanup] Unregistering agents...")
|
|
204
|
+
researcher.unregister()
|
|
205
|
+
search_service.unregister()
|
|
206
|
+
|
|
207
|
+
print("\n" + "=" * 60)
|
|
208
|
+
print("Example 1b Complete!")
|
|
209
|
+
print("First invocation required approval, subsequent were immediate.")
|
|
210
|
+
print("=" * 60)
|
|
211
|
+
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
import sys
|
|
217
|
+
try:
|
|
218
|
+
sys.exit(main())
|
|
219
|
+
except Exception as e:
|
|
220
|
+
err = str(e)
|
|
221
|
+
print("\n" + "=" * 60)
|
|
222
|
+
if "Local provider does not support" in err:
|
|
223
|
+
print("ERROR: Identity Provider not configured")
|
|
224
|
+
print("Fix: Console -> Settings -> Identity Bridge")
|
|
225
|
+
elif "Connection refused" in err or "connect" in err.lower():
|
|
226
|
+
print("ERROR: Cannot connect to MACAW")
|
|
227
|
+
print("Fix: Ensure LocalAgent is running")
|
|
228
|
+
else:
|
|
229
|
+
print(f"ERROR: {e}")
|
|
230
|
+
print("=" * 60)
|
|
231
|
+
sys.exit(1)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# External Attestations Examples
|
|
2
|
+
|
|
3
|
+
These examples demonstrate **human-in-the-loop approval workflows** using MACAW's external attestation system.
|
|
4
|
+
|
|
5
|
+
## What Are External Attestations?
|
|
6
|
+
|
|
7
|
+
External attestations require a **third party** (manager, admin, compliance officer) to approve an operation before it can proceed. The request **blocks** while waiting for approval.
|
|
8
|
+
|
|
9
|
+
**Use cases:**
|
|
10
|
+
- High-value trades requiring manager sign-off
|
|
11
|
+
- Sensitive operations needing compliance review
|
|
12
|
+
- Capability grants for elevated access
|
|
13
|
+
- Time-boxed permissions
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### Example 1a: One-Time Trade Approval
|
|
18
|
+
|
|
19
|
+
A high-value trade ($15,000) requires manager approval. Each trade needs fresh approval.
|
|
20
|
+
|
|
21
|
+
| File | Role | What It Does |
|
|
22
|
+
|------|------|--------------|
|
|
23
|
+
| `1a_trade_alice.py` | Requester | Requests trade, blocks waiting for approval |
|
|
24
|
+
| `1a_trade_bob.py` | Approver | Lists pending requests, approves/denies |
|
|
25
|
+
|
|
26
|
+
**Key concepts:**
|
|
27
|
+
- Conditional attestation: `trade-approved::{params.amount > 10000}`
|
|
28
|
+
- Role-based approval: `approval_criteria: "role:manager"`
|
|
29
|
+
- One-time use: `one_time: True` (consumed after single use)
|
|
30
|
+
|
|
31
|
+
**Run:**
|
|
32
|
+
```bash
|
|
33
|
+
# Terminal 1: Alice requests trade (blocks)
|
|
34
|
+
python 1a_trade_alice.py
|
|
35
|
+
|
|
36
|
+
# Terminal 2: Bob approves (while Alice waits)
|
|
37
|
+
python 1a_trade_bob.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Example 1b: Reusable Capability Grant
|
|
41
|
+
|
|
42
|
+
A researcher needs admin approval for web search capability. Approve once, use many times.
|
|
43
|
+
|
|
44
|
+
| File | Role | What It Does |
|
|
45
|
+
|------|------|--------------|
|
|
46
|
+
| `1b_grant_researcher.py` | Requester | First call blocks, subsequent calls instant |
|
|
47
|
+
| `1b_grant_admin.py` | Approver | Grants reusable capability |
|
|
48
|
+
|
|
49
|
+
**Key concepts:**
|
|
50
|
+
- Reusable grant: `one_time: False`
|
|
51
|
+
- Time-to-live: `time_to_live: 3600` (1 hour)
|
|
52
|
+
- First call blocks, second+ calls immediate
|
|
53
|
+
|
|
54
|
+
**Run:**
|
|
55
|
+
```bash
|
|
56
|
+
# Terminal 1: Researcher requests (blocks on first call)
|
|
57
|
+
python 1b_grant_researcher.py
|
|
58
|
+
|
|
59
|
+
# Terminal 2: Admin approves (while researcher waits)
|
|
60
|
+
python 1b_grant_admin.py
|
|
61
|
+
|
|
62
|
+
# Watch Terminal 1: First call completes, then 2nd and 3rd are instant!
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Prerequisites
|
|
66
|
+
|
|
67
|
+
1. **MACAW LocalAgent running:**
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m macaw_agent.main
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. **Identity provider configured** (Keycloak or Auth0)
|
|
73
|
+
- See `demos/tutorial-1/setup/` for setup scripts
|
|
74
|
+
|
|
75
|
+
3. **Users created in IDP:**
|
|
76
|
+
- `alice` - trader/researcher
|
|
77
|
+
- `bob` - manager/admin with appropriate role
|
|
78
|
+
|
|
79
|
+
## Key APIs
|
|
80
|
+
|
|
81
|
+
### Requester Side
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from macaw_client import MACAWClient
|
|
85
|
+
|
|
86
|
+
# Define attestation in policy
|
|
87
|
+
intent_policy = {
|
|
88
|
+
"attestations": ["trade-approved::{params.amount > 10000}"],
|
|
89
|
+
"constraints": {
|
|
90
|
+
"attestations": {
|
|
91
|
+
"trade-approved": {
|
|
92
|
+
"approval_criteria": "role:manager",
|
|
93
|
+
"timeout": 300, # Block up to 5 minutes
|
|
94
|
+
"one_time": True # Or False for reusable
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# invoke_tool blocks until approved/denied/timeout
|
|
101
|
+
result = client.invoke_tool("tool:trading/execute", params)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Approver Side
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from macaw_client import MACAWClient
|
|
108
|
+
|
|
109
|
+
# List pending attestations you can approve
|
|
110
|
+
attestations = client.list_attestations(status="pending")
|
|
111
|
+
|
|
112
|
+
# Approve or deny
|
|
113
|
+
client.approve_attestation(att, reason="Approved for Q4")
|
|
114
|
+
client.deny_attestation(att, reason="Budget exceeded")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## One-Time vs Reusable
|
|
118
|
+
|
|
119
|
+
| Setting | Behavior | Use Case |
|
|
120
|
+
|---------|----------|----------|
|
|
121
|
+
| `one_time: True` | Consumed after single use | High-value trades |
|
|
122
|
+
| `one_time: False` | Reusable until TTL expires | Capability grants |
|
|
123
|
+
|
|
124
|
+
## Learn More
|
|
125
|
+
|
|
126
|
+
- **MAPL Policy Guide**: Full attestation syntax and semantics
|
|
127
|
+
- **Dev Hub Tutorials**: Step-by-step walkthrough with diagrams
|
|
128
|
+
- **Console > Activity**: View attestation audit events
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
langchain_1a_dropin_simple.py - Drop-in replacement for LangChain agents
|
|
4
|
+
|
|
5
|
+
The simplest integration path - replace your LangChain imports and add policy.
|
|
6
|
+
No Identity Provider needed.
|
|
7
|
+
|
|
8
|
+
Prerequisites:
|
|
9
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
10
|
+
- OPENAI_API_KEY environment variable (for ChatOpenAI)
|
|
11
|
+
|
|
12
|
+
Run:
|
|
13
|
+
export OPENAI_API_KEY=sk-...
|
|
14
|
+
python langchain_1a_dropin_simple.py
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# BEFORE: from langchain.agents import create_react_agent, AgentExecutor
|
|
21
|
+
# AFTER:
|
|
22
|
+
from macaw_adapters.langchain import create_react_agent, AgentExecutor, cleanup
|
|
23
|
+
|
|
24
|
+
from langchain_openai import ChatOpenAI
|
|
25
|
+
from langchain.tools import Tool
|
|
26
|
+
from langchain.prompts import PromptTemplate
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Define some tools
|
|
30
|
+
def calculator(expression: str) -> str:
|
|
31
|
+
"""Evaluate a math expression."""
|
|
32
|
+
try:
|
|
33
|
+
result = eval(expression)
|
|
34
|
+
return f"Result: {result}"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return f"Error: {e}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_weather(city: str) -> str:
|
|
40
|
+
"""Get weather for a city (mock)."""
|
|
41
|
+
# Mock weather data
|
|
42
|
+
weather_data = {
|
|
43
|
+
"new york": "72°F, Sunny",
|
|
44
|
+
"london": "58°F, Cloudy",
|
|
45
|
+
"tokyo": "68°F, Clear"
|
|
46
|
+
}
|
|
47
|
+
return weather_data.get(city.lower(), f"Weather for {city}: 65°F, Partly cloudy")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def admin_tool(command: str) -> str:
|
|
51
|
+
"""Execute admin command (dangerous!)."""
|
|
52
|
+
return f"Executed admin command: {command}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Create tools list
|
|
56
|
+
TOOLS = [
|
|
57
|
+
Tool(
|
|
58
|
+
name="calculator",
|
|
59
|
+
func=calculator,
|
|
60
|
+
description="Useful for math calculations. Input should be a math expression."
|
|
61
|
+
),
|
|
62
|
+
Tool(
|
|
63
|
+
name="weather",
|
|
64
|
+
func=get_weather,
|
|
65
|
+
description="Get current weather for a city. Input should be a city name."
|
|
66
|
+
),
|
|
67
|
+
Tool(
|
|
68
|
+
name="admin",
|
|
69
|
+
func=admin_tool,
|
|
70
|
+
description="Execute admin commands. Input should be a command string."
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Simple ReAct prompt template
|
|
76
|
+
REACT_PROMPT = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:
|
|
77
|
+
|
|
78
|
+
{tools}
|
|
79
|
+
|
|
80
|
+
Use the following format:
|
|
81
|
+
|
|
82
|
+
Question: the input question you must answer
|
|
83
|
+
Thought: you should always think about what to do
|
|
84
|
+
Action: the action to take, should be one of [{tool_names}]
|
|
85
|
+
Action Input: the input to the action
|
|
86
|
+
Observation: the result of the action
|
|
87
|
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
|
88
|
+
Thought: I now know the final answer
|
|
89
|
+
Final Answer: the final answer to the original input question
|
|
90
|
+
|
|
91
|
+
Begin!
|
|
92
|
+
|
|
93
|
+
Question: {input}
|
|
94
|
+
Thought:{agent_scratchpad}""")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
# Check for API key
|
|
99
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
100
|
+
print("Set OPENAI_API_KEY environment variable")
|
|
101
|
+
print(" export OPENAI_API_KEY=sk-...")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
print("=" * 60)
|
|
105
|
+
print("Example 1a: Drop-in Replacement (LangChain)")
|
|
106
|
+
print("=" * 60)
|
|
107
|
+
print("\nPath: Direct agent with security_policy")
|
|
108
|
+
print("Use when: Single app, need tool access control")
|
|
109
|
+
|
|
110
|
+
# Create LLM
|
|
111
|
+
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
|
|
112
|
+
|
|
113
|
+
# Create agent WITH security policy
|
|
114
|
+
# - Only "calculator" and "weather" are allowed
|
|
115
|
+
# - "admin" tool is blocked
|
|
116
|
+
# - Queries containing "password" are blocked
|
|
117
|
+
print("\n--- Creating secure agent ---")
|
|
118
|
+
print("Policy: Allow calculator & weather, block admin")
|
|
119
|
+
print(" Block queries with '*password*'")
|
|
120
|
+
|
|
121
|
+
# MAPL format policy - direct, no conversion layer
|
|
122
|
+
policy = {
|
|
123
|
+
"resources": ["tool:calculator", "tool:weather"], # Allowed tools
|
|
124
|
+
"denied_resources": ["tool:admin"], # Blocked tools
|
|
125
|
+
"constraints": {
|
|
126
|
+
"denied_parameters": {
|
|
127
|
+
"tool:*": {"input": ["*password*", "*secret*"]} # Block sensitive queries
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
agent = create_react_agent(
|
|
133
|
+
llm=llm,
|
|
134
|
+
tools=TOOLS,
|
|
135
|
+
prompt=REACT_PROMPT,
|
|
136
|
+
security_policy=policy
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Create executor with same policy
|
|
140
|
+
executor = AgentExecutor(
|
|
141
|
+
agent=agent,
|
|
142
|
+
tools=TOOLS,
|
|
143
|
+
security_policy=policy,
|
|
144
|
+
verbose=True,
|
|
145
|
+
handle_parsing_errors=True,
|
|
146
|
+
max_iterations=3
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Test 1: Allowed tool (calculator)
|
|
150
|
+
print("\n--- Test 1: Allowed tool (calculator) ---")
|
|
151
|
+
try:
|
|
152
|
+
result = executor.invoke({"input": "What is 15 * 7 + 23?"})
|
|
153
|
+
print(f"Result: {result.get('output', result)}")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"Error: {e}")
|
|
156
|
+
|
|
157
|
+
# Test 2: Allowed tool (weather)
|
|
158
|
+
print("\n--- Test 2: Allowed tool (weather) ---")
|
|
159
|
+
try:
|
|
160
|
+
result = executor.invoke({"input": "What's the weather in Tokyo?"})
|
|
161
|
+
print(f"Result: {result.get('output', result)}")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"Error: {e}")
|
|
164
|
+
|
|
165
|
+
# Test 3: Blocked tool (admin) - should be denied
|
|
166
|
+
print("\n--- Test 3: Blocked tool (admin) - SHOULD BE DENIED ---")
|
|
167
|
+
try:
|
|
168
|
+
result = executor.invoke({"input": "Run admin command: list users"})
|
|
169
|
+
print(f"Result: {result.get('output', result)}")
|
|
170
|
+
# If agent can't use admin tool, it should say it can't do this
|
|
171
|
+
except Exception as e:
|
|
172
|
+
error_msg = str(e).lower()
|
|
173
|
+
if "denied" in error_msg or "blocked" in error_msg or "policy" in error_msg:
|
|
174
|
+
print(f"Correctly blocked: {e}")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Error: {e}")
|
|
177
|
+
|
|
178
|
+
# Cleanup
|
|
179
|
+
print("\n--- Cleanup ---")
|
|
180
|
+
cleanup()
|
|
181
|
+
|
|
182
|
+
print("\n" + "=" * 60)
|
|
183
|
+
print("Example complete!")
|
|
184
|
+
print("=" * 60)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
try:
|
|
189
|
+
main()
|
|
190
|
+
except Exception as e:
|
|
191
|
+
err = str(e)
|
|
192
|
+
print("\n" + "=" * 60)
|
|
193
|
+
if "OPENAI_API_KEY" in err or "api_key" in err.lower():
|
|
194
|
+
print("ERROR: OpenAI API key not configured")
|
|
195
|
+
print("Fix: export OPENAI_API_KEY=sk-...")
|
|
196
|
+
else:
|
|
197
|
+
print(f"ERROR: {e}")
|
|
198
|
+
print("=" * 60)
|
|
199
|
+
sys.exit(1)
|