@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.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +278 -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,213 @@
1
+ # Tutorial 2: Intent-Based Access Control
2
+
3
+ This tutorial demonstrates intent-based access controls, policy enforcement, model restrictions and
4
+ action-tracking for a fictional XYZ Corp. Controls are specified through MAPL policies enforced by
5
+ the MACAW runtime invisbly. We use Alation as the contextual data layer, Databricks as the backend
6
+ lakehouse, and OpenAI as the LLM of choice. All these components are replaceable modularly.
7
+
8
+ ## Overview
9
+
10
+ The demo shows how different users get different access control based on their org role,
11
+ demonstrating policy based control.
12
+
13
+ It also shows control by intent. The same tool call is judged by what the SQL is trying to do depending upon the policy.
14
+
15
+ | User | Role | Tool available | Model available | Max Token | SQL allowed | Needs approval when |
16
+ |------|------|----------------|-----------------|-----------|-------------|---------------------|
17
+ | alice | Financial Analyst | `run_query_sql_custom_adi` | `gpt-4o-mini` | 100 | select, update | any statement touching `eng_comp` |
18
+ | bob | manager | `run_query_sql_custom_adi`, `run_data_product_query_macaw_ii`, `run_analytics_agent_macaw`, `run_generate_pdf_from_a_chat_macaw` | `gpt-4o-mini`, `gpt-4o` | 2000 | select, update | an UPDATE touching `eng_comp` |
19
+
20
+
21
+
22
+ ## Directory Structure
23
+
24
+ ```
25
+ demo-alation/
26
+ ├── script.py # Main demo
27
+ ├── Policies/
28
+ │ ├── company.json # company:alation-MACAW
29
+ │ ├── bu.json # bu:analytics
30
+ │ ├── user_alice.json # user:alice
31
+ │ ├── user_bob.json # user:bob
32
+ │ └── alation-remote-proxy.json # app:alation-remote-proxy
33
+ ├── setup/
34
+ │ ├── alation_verifier.py # AlationSQLGuardVerifier
35
+ │ └── get_alation_user_token.py # Alation OAuth + PKCE token
36
+ └── README.md
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### 1. Prerequisites
42
+
43
+ - Python 3.12+
44
+ - MACAW console account
45
+ - macaw client installed and configured
46
+ - env's configured
47
+ - A alation tenant
48
+
49
+ ```bash
50
+ export ALATION_BASE_URL="https://<tenant>.alationcloud.com"
51
+ export ALATION_CLIENT_ID="<oauth client id>"
52
+ export ALATION_CLIENT_SECRET="<oauth client secret>"
53
+ export ALATION_MCP_URL="https://<tenant>.alationcloud.com/ai/mcp/<uuid>"
54
+ export OPENAI_API_KEY="<key>"
55
+ ```
56
+
57
+ ### 2. Install Dependencies
58
+
59
+ ```bash
60
+ pip install "$MACAW_HOME"/macaw_client-*.whl "$MACAW_HOME/secureAI[all]"
61
+ ```
62
+
63
+ ### 3. Set Up Identity Provider
64
+
65
+ Console → Tutorials → Make it Real → Connect Identity Provider → Option B.
66
+
67
+ When done with the setup follow this claims mapping:
68
+
69
+ ```yaml
70
+ name:
71
+ domain:
72
+ client_id:
73
+ client_secret:
74
+ api_audience:
75
+
76
+ mappings:
77
+ subject_path: sub
78
+ email_path: email
79
+ name_path: https://macaw.local/username
80
+ organization_path: https://macaw.local/organization
81
+ roles_path: https://macaw.local/roles
82
+ team_path: https://macaw.local/team
83
+ business_unit_path: https://macaw.local/business_unit
84
+ ```
85
+
86
+ #### Claims Mapping
87
+
88
+ Each mapped claim becomes the name of a policy to look up:
89
+
90
+ | Claim | Value | Policy |
91
+ |-------|-------|--------|
92
+ | `name_path` → username | `alice` | `user:alice` |
93
+ | `business_unit_path` | `analytics` | `bu:analytics` |
94
+ | `organization_path` | `alation-MACAW` | `company:alation-MACAW` |
95
+ | `roles_path` | `["manager"]` | matches `approval_criteria: role:manager` |
96
+
97
+ This allows the same policies to work with any OIDC-compliant IdP.
98
+
99
+ ### 4. Get the Alation Token
100
+
101
+ To get alation token run, `setup/get_alation_user_token.py`
102
+
103
+ This open a browser where you login to get a access token and a refresh token.
104
+
105
+ ```bash
106
+ export ALATION_TOKEN="<the access token>"
107
+ ```
108
+
109
+ Later you can use the same refresh token to get a new access token without the going through the
110
+ browser again.
111
+
112
+ ```bash
113
+ python setup/get_alation_user_token.py --refresh <REFRESH_TOKEN>
114
+ ```
115
+
116
+ ### 5. Load Policies
117
+
118
+ Import the policies from the `Policies/` directory into your MACAW workspace via the Console.
119
+ Load each one: Policies → Add Policy → Code Editor → paste JSON → Validate → Save.
120
+
121
+ | # | File | Policy id |
122
+ |---|---|---|
123
+ | 1 | `Policies/company.json` | `company:alation-MACAW` |
124
+ | 2 | `Policies/bu.json` | `bu:analytics` |
125
+ | 3 | `Policies/user_alice.json` | `user:alice` |
126
+ | 4 | `Policies/user_bob.json` | `user:bob` |
127
+ | 5 | `Policies/alation-remote-proxy.json` | `app:alation-remote-proxy` |
128
+
129
+ #### Policy Hierarchy
130
+
131
+ The policies demonstrate MACAW's hierarchical policy model:
132
+
133
+ ```
134
+ company:alation-MACAW (base restrictions)
135
+ └── bu:analytics (business unit)
136
+ └── user:bob (individual user)
137
+ └── user:alice (individual user)
138
+
139
+ app:alation-remote-proxy
140
+ ```
141
+
142
+ Lower levels can only restrict, never expand permissions from parent policies.
143
+
144
+ ### 6. Run the Demo
145
+
146
+ ```bash
147
+ export MACAW_HOME="<path to macaw-client-0.9.9.6-Linux-x86_64-py3.12>"
148
+ export ALATION_MCP_URL="https://<tenant>.alationcloud.com/ai/mcp/<uuid>"
149
+ export ALATION_TOKEN="<fresh 72h bearer>" # from setup/get_alation_user_token.py
150
+ export OPENAI_API_KEY="<key>"
151
+
152
+ python script.py
153
+ ```
154
+
155
+
156
+
157
+ ## Custom Verifier
158
+
159
+ Custom verifiers run inside the verification pipeline before the policy decision, compute facts
160
+ about the request, and stamp them onto the parameters so MAPL can gate on them.
161
+
162
+ `setup/alation_verifier.py` — AlationSQLGuardVerifier
163
+
164
+ ```python
165
+ proxy.macaw_client.agent.verification_pipeline.add_verifier(AlationSQLGuardVerifier(), priority=20)
166
+ ```
167
+
168
+ It parses the SQL with sqlglot and stamps three parameters:
169
+
170
+ | Parameter | Values | Comes from |
171
+ |-----------|--------|------------|
172
+ | `stmt_type` | select, update, insert, delete, merge, create, drop, truncate, other, nl_only, denied | the AST root node |
173
+ | `touches_salary` | true / false | any identifier containing salary (SENSITIVE_TOKENS) |
174
+ | `touches_eng_comp` | true / false | any table in SENSITIVE_TABLES = ("eng_comp",) |
175
+
176
+ | What it does | Why |
177
+ |--------------|-----|
178
+ | Looks at what the statement really is, not the words in it | INSERT … SELECT has the word SELECT in it, but it writes |
179
+ | Only the types it recognises get through | anything else is marked other, and no policy allows other |
180
+ | If it cannot read the SQL, it says no | empty, two statements at once, or unparseable → denied |
181
+ | A SQL tool called with no SQL is refused | otherwise Alation writes the SQL itself, where we cannot see it |
182
+ | Matches the table name however it is written | workspace.macaw_demo.eng_comp matches, eng_comp_archive does not |
183
+ | Checks every SQL field and keeps the worst answer | a safe sql cannot hide a dangerous query |
184
+
185
+
186
+ ## What the Tutorial Shows
187
+
188
+ 1. **Authentication**: Each user authenticates with the IdP (Keycloak/Auth0)
189
+ 2. **Policy Resolution**: MACAW resolves the user's effective policy from the hierarchy
190
+ 3. **Enforcement**: Requests are allowed or blocked based on policy rules
191
+
192
+ ### Expected Output
193
+
194
+ **Bob**
195
+
196
+ | # | Call | Request | Result | What MACAW said |
197
+ |---|---|---|---|---|
198
+ | 1 | LLM | `gpt-4-turbo`, 80 tokens | 🔴 BLOCKED | `Parameter 'model'=gpt-4-turbo not in allowed values: ['gpt-4o-mini', 'gpt-4o']` |
199
+ | 2 | LLM | `gpt-4o`, 3000 tokens | 🔴 BLOCKED | `Parameter 'max_tokens'=3000 exceeds maximum: 2000` |
200
+ | 3 | LLM | `gpt-4o`, 500 tokens | 🟢 ALLOWED | *"Compound interest is the process where interest is added to the principal…"* |
201
+ | 4 | SQL | `SELECT name, base_salary FROM workspace.macaw_demo.eng_comp LIMIT 3` | 🟢 ALLOWED | rows returned |
202
+ | 5 | SQL | `SELECT COUNT(*) FROM workspace.macaw_demo.eng_comp` | 🟢 ALLOWED | `COUNT(*) = 4` |
203
+
204
+ **Alice**
205
+
206
+ | # | Call | Request | Result | What MACAW said |
207
+ |---|---|---|---|---|
208
+ | 1 | LLM | `gpt-4o`, 80 tokens | 🔴 BLOCKED | `Parameter 'model'=gpt-4o not in allowed values: ['gpt-4o-mini']` |
209
+ | 2 | LLM | `gpt-4o-mini`, 300 tokens | 🔴 BLOCKED | `Parameter 'max_tokens'=300 exceeds maximum: 100` |
210
+ | 3 | LLM | `gpt-4o-mini`, 80 tokens | 🟢 ALLOWED | *"Compound interest is the interest calculated on the initial principal…"* |
211
+ | 4 | SQL | `DELETE FROM workspace.macaw_demo.eng_comp WHERE name = 'nobody'` | 🔴 BLOCKED | `Parameter 'stmt_type'=delete not in allowed values: ['select', 'update']` |
212
+ | 5 | SQL | `SELECT name, base_salary FROM workspace.macaw_demo.eng_comp LIMIT 3` | 🟡 ATTESTATION | `Missing or invalid attestation: eng_comp_attestation` |
213
+
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ script.py - Per-user LLM and Alation SQL access, gated by MACAW
4
+
5
+ alice (manager) and bob (analyst) each make 3 LLM calls and 2 Alation SQL calls
6
+ through identical code. MAPL policy is evaluated per request against the calling
7
+ user's identity, so the same call succeeds for one user and is blocked or held
8
+ for approval for the other.
9
+
10
+ Prerequisites:
11
+ - MACAW SDK installed (pip install macaw-client macaw-adapters)
12
+ - Identity Provider configured (Console -> Settings -> Identity Bridge)
13
+ - Policies loaded in the Console (see Policies/)
14
+ - Test users: alice.alation@gmail.com, bob.alation@gmail.com
15
+
16
+ Run:
17
+ export MACAW_HOME=.../macaw-client-0.9.9.6-Linux-x86_64-py3.12
18
+ export ALATION_MCP_URL=https://<tenant>.alationcloud.com/ai/mcp/<uuid>
19
+ export ALATION_TOKEN=<fresh bearer>
20
+ export OPENAI_API_KEY=sk-...
21
+ python script.py
22
+ """
23
+
24
+ import os
25
+ import sys
26
+ import logging
27
+
28
+ import httpx
29
+
30
+ from macaw_adapters.openai import SecureOpenAI
31
+ from macaw_adapters.mcp import SecureMCPProxy
32
+ from macaw_client import MACAWClient, RemoteIdentityProvider
33
+
34
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "setup"))
35
+ from alation_verifier import AlationSQLGuardVerifier
36
+
37
+ logging.getLogger("macaw_client").setLevel(logging.ERROR)
38
+
39
+ PASSWORD = "test@123"
40
+ PROMPT = "What is compound interest? Answer in one short sentence."
41
+ DATA_PRODUCT = "databricks-macaw-product"
42
+ ENG_COMP = "workspace.macaw_demo.eng_comp" # Databricks will not resolve a bare 'eng_comp'
43
+
44
+
45
+ # Test configurations based on user policies
46
+ # Alice: gpt-4o-mini/gpt-4o, max 2000 tokens; reads eng_comp freely
47
+ # Bob: gpt-4o-mini only, max 100 tokens; eng_comp needs a manager's attestation
48
+ USER_TESTS = {
49
+ "bob": {
50
+ "email": "bob.alation@gmail.com",
51
+ "policy_desc": "gpt-4o-mini/gpt-4o, max 2000 tokens",
52
+ "llm": [
53
+ # (model, max_tokens)
54
+ ("gpt-4-turbo", 80), # BLOCKED - model not allowed
55
+ ("gpt-4o", 3000), # BLOCKED - exceeds token ceiling
56
+ ("gpt-4o", 500), # ALLOWED
57
+ ],
58
+ "sql": [
59
+ f"SELECT name, base_salary FROM {ENG_COMP} LIMIT 3", # ALLOWED
60
+ f"SELECT COUNT(*) FROM {ENG_COMP}", # ALLOWED
61
+ ],
62
+ },
63
+ "alice": {
64
+ "email": "alice.alation@gmail.com",
65
+ "policy_desc": "gpt-4o-mini only, max 100 tokens",
66
+ "llm": [
67
+ ("gpt-4o", 80), # BLOCKED - model not allowed
68
+ ("gpt-4o-mini", 300), # BLOCKED - exceeds token ceiling
69
+ ("gpt-4o-mini", 80), # ALLOWED
70
+ ],
71
+ "sql": [
72
+ f"DELETE FROM {ENG_COMP} WHERE name = 'nobody'", # BLOCKED - stmt_type
73
+ f"SELECT name, base_salary FROM {ENG_COMP} LIMIT 3", # ATTESTATION
74
+ ],
75
+ },
76
+ }
77
+
78
+
79
+ def get_env(name: str) -> str:
80
+ """Read a required environment variable.
81
+
82
+ Strips surrounding quotes, including the curly ones a copy-paste from a document
83
+ leaves behind - those reach the Authorization header and fail as a bad credential.
84
+ """
85
+ value = os.environ.get(name, "").strip().strip("\"'“”‘’")
86
+ if not value:
87
+ sys.exit(f"Missing {name}. Set it with: export {name}=...")
88
+ if not value.isascii():
89
+ sys.exit(f"{name} contains a non-ASCII character - re-copy it as plain text.")
90
+ return value
91
+
92
+
93
+ def verdict_for(error: Exception) -> str:
94
+ """Separate a MACAW policy decision from any other failure.
95
+
96
+ A demo about policy enforcement must never report a broken credential or a network
97
+ fault as though the policy had blocked it.
98
+ """
99
+ message = str(error).lower()
100
+ if "attest" in message:
101
+ return "ATTESTATION"
102
+ if any(s in message for s in ("not in allowed", "exceeds maximum", "policy",
103
+ "denied", "not permitted")):
104
+ return "BLOCKED"
105
+ return f"ERROR - {str(error)[:70]}"
106
+
107
+
108
+ def patch_proxy_timeout():
109
+ """Tune the upstream HTTP client for Alation's long-running agent endpoint.
110
+
111
+ httpx ships a 5s default that suits short request/response APIs. Here we widen the
112
+ read timeout to 300s for this upstream while keeping connect, write and pool tight,
113
+ so long analytical calls complete without loosening anything else.
114
+ """
115
+ def create_http_client(self):
116
+ auth = self.upstream_auth
117
+ headers = ({"Authorization": f"Bearer {auth.token}"}
118
+ if getattr(auth, "type", None) == "bearer" and getattr(auth, "token", None)
119
+ else {})
120
+ return httpx.AsyncClient(
121
+ headers=headers or None,
122
+ timeout=httpx.Timeout(connect=30, read=300, write=30, pool=30),
123
+ )
124
+ SecureMCPProxy._create_http_client = create_http_client
125
+
126
+
127
+ def test_user(username: str, openai_service: SecureOpenAI, proxy: SecureMCPProxy):
128
+ """Run one user's LLM and SQL calls under their own identity."""
129
+ config = USER_TESTS[username]
130
+ print(f"\n{'=' * 60}")
131
+ print(f"{username.upper()} - {config['policy_desc']}")
132
+ print("=" * 60)
133
+
134
+ # 1. Authenticate and register the user agent
135
+ jwt_token, _ = RemoteIdentityProvider().login(config["email"], PASSWORD)
136
+ user = MACAWClient(user_name=username, iam_token=jwt_token,
137
+ agent_type="user", app_name="alation")
138
+ user.register()
139
+
140
+ # 2. LLM calls - A2A to the shared OpenAI service, gated on model and max_tokens
141
+ for model, max_tokens in config["llm"]:
142
+ try:
143
+ result = user.invoke_tool(
144
+ tool_name="tool:openai-service/generate",
145
+ parameters={
146
+ "model": model,
147
+ "max_tokens": max_tokens,
148
+ "messages": [{"role": "user", "content": PROMPT}],
149
+ },
150
+ target_agent=openai_service.server_id,
151
+ )
152
+ answer = result["choices"][0]["message"]["content"].strip()
153
+ print(f" LLM {model} /{max_tokens} -> {answer}")
154
+ except Exception as e:
155
+ print(f" LLM {model} /{max_tokens} -> {verdict_for(e)}")
156
+
157
+ # 3. SQL calls - through the user-bound proxy, gated by the verifier and policy
158
+ bound = proxy.bind_to_user(user)
159
+ for sql in config["sql"]:
160
+ try:
161
+ result = bound.call_tool(
162
+ "run_query_sql_custom_adi",
163
+ {"sql": sql, "message": "demo", "data_product_id": DATA_PRODUCT},
164
+ )
165
+ print(f" SQL {sql}\n -> {str(result)[:220]}")
166
+ except Exception as e:
167
+ print(f" SQL {sql} -> {verdict_for(e)}")
168
+
169
+
170
+ def main():
171
+ openai_service = SecureOpenAI(api_key=get_env("OPENAI_API_KEY"), app_name="openai-service")
172
+
173
+ patch_proxy_timeout()
174
+ proxy = SecureMCPProxy(
175
+ app_name="alation-remote-proxy",
176
+ upstream_url=get_env("ALATION_MCP_URL"),
177
+ upstream_auth={"type": "bearer", "token": get_env("ALATION_TOKEN")},
178
+ )
179
+ proxy.macaw_client.agent.verification_pipeline.add_verifier(
180
+ AlationSQLGuardVerifier(), priority=20
181
+ )
182
+
183
+ for username in USER_TESTS:
184
+ test_user(username, openai_service, proxy)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
@@ -0,0 +1,177 @@
1
+ """
2
+ alation_verifier.py -- SQL statement-type guardrail for the Alation MCP SQL tools.
3
+
4
+ Parses the caller's SQL and stamps THREE flat params the MAPL policy gates on:
5
+
6
+ stmt_type = "select" | "update" | "insert" | "delete" | "merge" | "create"
7
+ | "drop" | "truncate" | "other" | "nl_only" | "denied"
8
+ ("denied" = fail-closed; "nl_only" = a SQL tool called with NO SQL)
9
+ touches_salary = "true" | "false" (does any identifier in the SQL contain "salary"?)
10
+ touches_eng_comp = "true" | "false" (does the SQL reference a SENSITIVE_TABLES table?)
11
+
12
+ The verifier CLASSIFIES; the policy DECIDES:
13
+
14
+ A) ANALYST (bob) -> ANY statement touching eng_comp attests to a manager;
15
+ other tables are ungated (within the {select, update} allow-list).
16
+ B) MANAGER (alice) -> only an UPDATE touching eng_comp attests (to an admin);
17
+ everything else within the allow-list is ungated.
18
+ C) DELETE -> denied for everyone. So is anything outside the {select, update}
19
+ allow-list (insert / merge / CTAS / drop / truncate / other).
20
+
21
+ touches_eng_comp closes the `SELECT *` hole in touches_salary: `SELECT * FROM eng_comp` names
22
+ no column identifier, so salary detection misses it, but the TABLE is still named.
23
+
24
+ SALARY DETECTION scans EVERY identifier in the whole AST -- projection AND WHERE / ORDER BY /
25
+ GROUP BY / JOIN / CTE / INSERT column-list -- because `WHERE base_salary > 200000` leaks salary
26
+ by row selection without projecting it. Identifiers are normalized first (Databricks is
27
+ CASE_INSENSITIVE, backticks stripped) so `Base_Salary` / `BASE_SALARY` / `` `base_salary` ``
28
+ all match. KNOWN GAP: `SELECT *` names no identifier, so it does NOT trip salary detection
29
+ (the statement-type allow-list still applies).
30
+
31
+ DESIGN (all verified against sqlglot 30.12.0, dialect="databricks"):
32
+ - Classify by the ROOT node ONLY, never "contains a SELECT" -- INSERT / CREATE-AS / MERGE all
33
+ embed a Select, so a contains-check is trivially fooled.
34
+ - ALLOW-LIST, not blocklist: anything unrecognized becomes "other" and the policy denies it.
35
+ DROP / TRUNCATE / MERGE / CTAS / INSERT OVERWRITE all destroy data without the word DELETE.
36
+ - FAIL CLOSED. Empty sql, multi-statement (`SELECT 1; DELETE ...`), unparseable SQL, or an
37
+ `exp.Command` stamp stmt_type="denied". `exp.Command` is sqlglot's fallback for unsupported
38
+ syntax and covers BOTH dynamic SQL (`EXECUTE IMMEDIATE`) and `ALTER ... DROP COLUMN`.
39
+ - MULTI-PARAM: the live endpoint's `run_analytics_agent_macaw` exposes BOTH `sql` and `query`.
40
+ Every name in SQL_PARAM_NAMES is classified and the WORST result wins, so a benign `sql`
41
+ cannot mask a destructive `query`. Checking only one name is a free bypass.
42
+ - A scoped SQL tool called with NO SQL is stamped "nl_only": the agent would generate the SQL
43
+ server-side where this verifier can never see it, so it is denied by the policy allow-list.
44
+ """
45
+ from typing import Tuple # noqa: F401 (kept for callers importing type hints)
46
+
47
+ import sqlglot
48
+ from sqlglot import expressions as exp
49
+ from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
50
+
51
+ DIALECT = "databricks" # MUST match the executor's dialect
52
+ SENSITIVE_TOKENS = ("salary",) # substring match on identifiers; add "bonus","ssn" to extend
53
+
54
+ # Guarded tables, matched on the TABLE-NAME SEGMENT ONLY (exact, after normalization) so that
55
+ # `eng_comp`, `macaw_demo.eng_comp` and `workspace.macaw_demo.eng_comp` all match the same entry
56
+ # -- the live endpoint requires the fully-qualified form, but an unqualified name must not slip
57
+ # past. Exact (not substring) so a distinct table like `eng_comp_archive` is NOT silently caught.
58
+ SENSITIVE_TABLES = ("eng_comp",)
59
+
60
+ # Every param that can carry raw SQL on the scoped tools. VERIFIED from the live endpoint:
61
+ # run_analytics_agent_macaw exposes BOTH `sql` and `query`; the others expose `sql`.
62
+ SQL_PARAM_NAMES = ("sql", "query")
63
+
64
+ # ROOT expression class -> stmt_type. Verified on sqlglot 30.12.0 / databricks:
65
+ # WITH..SELECT -> Select, UNION -> Union, INSERT OVERWRITE -> Insert, TRUNCATE -> TruncateTable.
66
+ _ROOT_MAP = (
67
+ (exp.Select, "select"),
68
+ (exp.Union, "select"), # a set operation over SELECTs is still a read
69
+ (exp.Update, "update"),
70
+ (exp.Insert, "insert"),
71
+ (exp.Delete, "delete"),
72
+ (exp.Merge, "merge"),
73
+ (exp.Create, "create"),
74
+ (exp.Drop, "drop"),
75
+ (exp.TruncateTable, "truncate"),
76
+ )
77
+
78
+ # Worst-wins ranking when a call carries more than one SQL param.
79
+ _SEVERITY = {
80
+ "select": 10, "update": 20, "insert": 50, "delete": 60, "merge": 60,
81
+ "create": 60, "drop": 70, "truncate": 70, "other": 80, "nl_only": 90, "denied": 99,
82
+ }
83
+
84
+
85
+ def classify_sql(sql: str) -> Tuple[str, bool, bool]:
86
+ """Return (stmt_type, touches_salary, touches_sensitive_table) for exactly ONE statement.
87
+
88
+ Raises for every input that must FAIL CLOSED: empty, multi-statement, `exp.Command`
89
+ (dynamic / unsupported syntax), or unparseable (sqlglot.ParseError propagates).
90
+ """
91
+ if not sql or not sql.strip():
92
+ raise ValueError("empty sql")
93
+ stmts = sqlglot.parse(sql, read=DIALECT)
94
+ if len(stmts) != 1 or stmts[0] is None:
95
+ raise ValueError(f"expected exactly 1 statement, got {len(stmts)}")
96
+ root = stmts[0]
97
+ if isinstance(root, exp.Command): # EXECUTE IMMEDIATE, ALTER ..., SET ...
98
+ raise ValueError("dynamic or unsupported statement (parsed as Command)")
99
+ root = normalize_identifiers(root, dialect=DIALECT)
100
+
101
+ stmt_type = "other"
102
+ for cls, name in _ROOT_MAP:
103
+ if isinstance(root, cls):
104
+ stmt_type = name
105
+ break
106
+
107
+ toks = tuple(t.lower() for t in SENSITIVE_TOKENS)
108
+ touches = any(any(k in i.name.lower() for k in toks) for i in root.find_all(exp.Identifier))
109
+
110
+ # .name is the table segment only -- catalog/schema are .catalog/.db -- so a guarded table is
111
+ # caught however the caller qualifies it. Covers the UPDATE/DELETE target and every FROM/JOIN.
112
+ guarded = tuple(t.lower() for t in SENSITIVE_TABLES)
113
+ touches_table = any(t.name.lower() in guarded for t in root.find_all(exp.Table))
114
+ return stmt_type, touches, touches_table
115
+
116
+
117
+ # --- SDK glue (lazy import so classify_sql stays importable/testable without the SDK) --------
118
+ # NOTE: `macaw` is a submodule injected by the macaw_client .so, so it imports only AFTER
119
+ # macaw_client has been imported (the gateway imports macaw_adapters/macaw_client first).
120
+ try:
121
+ from macaw.security.verification.verifier import Verifier
122
+ from macaw.security.verification.result import VerificationResult
123
+
124
+ class AlationSQLGuardVerifier(Verifier):
125
+ """Stamps params.stmt_type so the MAPL policy can gate A/B/C.
126
+
127
+ Always returns success=True -- the DENY decision belongs to the policy. Fail-closed
128
+ inputs are stamped stmt_type="denied", which is outside the policy's allowed_values
129
+ and therefore hard-denied.
130
+ """
131
+
132
+ def __init__(self):
133
+ super().__init__(name="alation_sql_guard_verifier")
134
+ # EVERY SQL-bearing tool on the endpoint. Verified live: leaving one out is a free
135
+ # bypass (call the unscoped agent instead). Re-check after publishing a new agent.
136
+ self.scope.resource_patterns = [
137
+ "*run_query_sql_custom_adi",
138
+ "*run_analytics_agent_macaw",
139
+ "*run_data_product_query_macaw_ii",
140
+ "*sql_execution_tool", # not currently published; kept for legacy/return
141
+ ]
142
+ self.scope.compile_patterns()
143
+
144
+ def verify(self, invocation, context):
145
+ params = invocation.params # mutate the REAL params, never a copy
146
+ if params is None:
147
+ return VerificationResult(True, "no params to gate")
148
+
149
+ sqls = [str(params[p]) for p in SQL_PARAM_NAMES
150
+ if params.get(p) and str(params[p]).strip()]
151
+ if not sqls:
152
+ # Scoped SQL tool called with NO SQL: the agent would write the SQL server-side
153
+ # where this verifier can never see it -- allowing that is a free bypass
154
+ # ("in English: delete the rows"). Not in the policy allow-list.
155
+ params["stmt_type"] = "nl_only"
156
+ params["touches_salary"] = "true" # unknown -> conservative
157
+ params["touches_eng_comp"] = "true" # unknown -> conservative
158
+ return VerificationResult(True, "no SQL param -> nl_only (fail-closed)")
159
+
160
+ results = []
161
+ for s in sqls:
162
+ try:
163
+ results.append(classify_sql(s))
164
+ except Exception: # noqa: BLE001 -- ParseError, ValueError, anything
165
+ results.append(("denied", True, True)) # fail closed
166
+
167
+ # Worst classification wins, so a benign `sql` can't mask a destructive `query`.
168
+ stmt_type = max((r[0] for r in results), key=lambda t: _SEVERITY.get(t, 99))
169
+ touches = any(r[1] for r in results)
170
+ touches_table = any(r[2] for r in results)
171
+ params["stmt_type"] = stmt_type
172
+ params["touches_salary"] = "true" if touches else "false"
173
+ params["touches_eng_comp"] = "true" if touches_table else "false"
174
+ return VerificationResult(
175
+ True, f"{stmt_type}/salary={touches}/eng_comp={touches_table}")
176
+ except ImportError:
177
+ pass # classify_sql remains usable without macaw_client loaded