@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,124 @@
1
+ # Phishing-Resistance Examples
2
+
3
+ These examples demonstrate **login-strength gating**: a tool runs only if the
4
+ human behind the agent authenticated with a **phishing-resistant** method
5
+ (passkey / hardware key / WebAuthn).
6
+
7
+ ## What Is Phishing-Resistance?
8
+
9
+ When a user authenticates, their token carries an `amr` (authentication
10
+ methods) claim describing *how* they logged in — `fido`/`hwk` for a passkey,
11
+ `pwd` for a password. At registration the control plane reads `amr` and, if the
12
+ login was phishing-resistant, mints an **internal** `phishing_resistant` grant
13
+ for that agent. A policy that lists `phishing_resistant` then allows or denies
14
+ with **no human in the loop and no waiting**.
15
+
16
+ This is the opposite of an [external attestation](../attestations/) like
17
+ `trade-approved`, which *blocks* while a manager approves:
18
+
19
+ | | `phishing_resistant` | `trade-approved` |
20
+ |---|---|---|
21
+ | decided by | the control plane, at login, from `amr` | a human approver (role:manager) |
22
+ | flow | deny-if-absent, immediate | blocks and waits |
23
+ | console | an **Active Grant** (never a pending request) | a pending HITL request |
24
+ | reusable | yes (`one_time=False`, TTL = token lifetime) | configurable |
25
+
26
+ **Use cases:** gating high-risk tools behind hardware-backed auth; requiring
27
+ step-up/passkey login before sensitive operations; layering a login-strength
28
+ check in front of a human approval.
29
+
30
+ ## Getting a token
31
+
32
+ Both examples read a token from `get_token.py`, which signs you in to Entra and
33
+ saves an id_token that carries `amr`:
34
+
35
+ ```bash
36
+ python get_token.py # a browser opens — choose how to sign in:
37
+ # PASSKEY -> amr:["fido"] -> ALLOW
38
+ # PASSWORD -> amr:["pwd","mfa"] -> DENY
39
+ export ALICE_TOKEN=$(cat ~/.macaw_demo_token)
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ ### Example 1a: Login gating (Alice alone)
45
+
46
+ The simplest form — one agent, one gate, **no approver**. Run it twice, changing
47
+ only how you log in.
48
+
49
+ | File | Role | What It Does |
50
+ |------|------|--------------|
51
+ | `1a_login_alice.py` | User | Invokes a `phishing_resistant`-gated tool; allowed or denied by login method |
52
+
53
+ **Key concepts:**
54
+ - The only gate: `"attestations": ["phishing_resistant"]`
55
+ - No `constraints` block, no approver, no blocking — the decision is immediate
56
+ - `PASSKEY` → allowed; `PASSWORD` → denied
57
+
58
+ **Run:**
59
+ ```bash
60
+ python get_token.py # sign in with a PASSKEY
61
+ export ALICE_TOKEN=$(cat ~/.macaw_demo_token)
62
+ python 1a_login_alice.py # ALLOWED
63
+
64
+ # now re-run get_token.py, sign in with a PASSWORD, and repeat:
65
+ python 1a_login_alice.py # DENIED at phishing_resistant
66
+ ```
67
+
68
+ ### Example 1b: Precondition to human approval (Alice + Bob)
69
+
70
+ Builds on the [alice/bob trade example](../attestations/) by **layering two
71
+ gates** on the same trade: `phishing_resistant` (internal, first) then
72
+ `trade-approved` (human, blocking, only when amount > $10,000).
73
+
74
+ | File | Role | What It Does |
75
+ |------|------|--------------|
76
+ | `1b_trade_alice.py` | Requester | Clears the phishing gate, then blocks for manager approval |
77
+ | `1b_trade_bob.py` | Approver | Lists pending requests, approves/denies the trade |
78
+
79
+ **Key concepts:**
80
+ - Two attestations in one policy: `["phishing_resistant", "trade-approved::{params.amount > 10000}"]`
81
+ - `phishing_resistant` is checked **first** — a weak login is denied before the trade ever reaches the manager gate
82
+ - On the DENY (password) run, `bob` is never involved
83
+
84
+ **Run:**
85
+ ```bash
86
+ python get_token.py # sign in with a PASSKEY
87
+ export ALICE_TOKEN=$(cat ~/.macaw_demo_token)
88
+
89
+ # Terminal 1: Alice (clears phishing gate, then blocks on approval)
90
+ python 1b_trade_alice.py
91
+
92
+ # Terminal 2: Bob approves
93
+ python 1b_trade_bob.py
94
+ ```
95
+
96
+ ## Prerequisites
97
+
98
+ 1. **MACAW control plane running** (LocalAgent):
99
+ ```bash
100
+ python3 -m macaw_agent.main
101
+ ```
102
+ 2. **Entra (Azure AD) connector configured** — Console → Settings → Identity
103
+ Bridge. `get_token.py` uses Entra's v1.0 endpoint because only v1.0 id_tokens
104
+ carry `amr`.
105
+ 3. **An Entra user with a passkey enrolled** (for the ALLOW run). Sign in with a
106
+ password to see the DENY run.
107
+ 4. **For 1b only:** an approver (`bob`) with `role:manager` — same setup as the
108
+ [attestations](../attestations/) demo.
109
+
110
+ ## How the grant works (under the hood)
111
+
112
+ - Captured at **login** (`iam_validate`) from the raw token `amr`; the grant is
113
+ minted at **registration**, keyed by the agent id (`alice@trading-app`),
114
+ signed by the tenant attestor, `one_time=False`, TTL = token lifetime.
115
+ - A weaker re-login (password after passkey) **revokes** the prior grant, so a
116
+ session can't inherit a stronger login's resistance.
117
+ - It is an **internal** grant: it never appears as an approvable request in the
118
+ console — it shows read-only under **Active Grants**.
119
+
120
+ ## Learn More
121
+
122
+ - [`examples/attestations/`](../attestations/) — external (human) attestations
123
+ - **MAPL Policy Guide** — full attestation syntax and semantics
124
+ - **Console → Activity** — view the grant and its audit events
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """Interactive Entra login -> v1.0 id_token (with amr) for the MACAW phishing-resistance demo.
3
+
4
+ Fill in the CONFIG below (or set ENTRA_TENANT_ID / ENTRA_CLIENT_ID). Run it, then sign in
5
+ in the browser that opens:
6
+
7
+ - PASSKEY (Touch ID / security key) -> amr:["fido"] -> phishing-resistant (ALLOW run)
8
+ - PASSWORD -> amr:["pwd","mfa"] -> not (DENY run)
9
+
10
+ It stores the token to ~/.macaw_demo_token (0600) and prints the export line the example reads.
11
+
12
+ Notes:
13
+ - Uses Entra's v1.0 endpoint (/oauth2/authorize) because only v1.0 id_tokens carry `amr`.
14
+ - Implicit flow (response_type=id_token) -> no client secret needed.
15
+ - Standalone: Python stdlib only, imports nothing from the client. No wheel/Nuitka build.
16
+
17
+ One-time Entra setup: App registration -> Authentication -> add redirect URI
18
+ http://localhost:8400 (Web platform), and ensure "ID tokens (implicit)" is enabled.
19
+ """
20
+ import os
21
+ import sys
22
+ import json
23
+ import base64
24
+ import secrets
25
+ import threading
26
+ import webbrowser
27
+ import urllib.parse
28
+ from http.server import BaseHTTPRequestHandler, HTTPServer
29
+
30
+ # ---- CONFIG (edit these, or set the env vars) ------------------------------
31
+ # Your Entra app registration's directory (tenant) ID and application (client) ID.
32
+ TENANT_ID = os.environ.get("ENTRA_TENANT_ID", "YOUR_ENTRA_TENANT_ID")
33
+ CLIENT_ID = os.environ.get("ENTRA_CLIENT_ID", "YOUR_ENTRA_CLIENT_ID")
34
+ PORT = 8400
35
+ # ----------------------------------------------------------------------------
36
+
37
+ REDIRECT = f"http://localhost:{PORT}"
38
+ TOKEN_FILE = os.path.expanduser("~/.macaw_demo_token")
39
+ AUTHORIZE = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/authorize"
40
+ PHISHING_RESISTANT_AMR = {"fido", "fido2", "hwk", "ngcmfa", "phr", "phrh"}
41
+
42
+ _result = {}
43
+
44
+
45
+ class Handler(BaseHTTPRequestHandler):
46
+ def do_GET(self):
47
+ parsed = urllib.parse.urlparse(self.path)
48
+ if parsed.path == "/":
49
+ # The id_token arrives in the URL fragment (#...), which browsers do not send to
50
+ # the server. Serve a tiny page that bounces the fragment to /capture as a query.
51
+ self._html("<script>location.replace('/capture?'+location.hash.substring(1))</script>")
52
+ elif parsed.path == "/capture":
53
+ _result.update({k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()})
54
+ self._html("<h3>Login complete — you can close this tab.</h3>")
55
+ else:
56
+ self.send_response(404)
57
+ self.end_headers()
58
+
59
+ def _html(self, body):
60
+ self.send_response(200)
61
+ self.send_header("Content-Type", "text/html")
62
+ self.end_headers()
63
+ self.wfile.write(body.encode())
64
+
65
+ def log_message(self, *args):
66
+ pass # quiet
67
+
68
+
69
+ def main():
70
+ params = {
71
+ "client_id": CLIENT_ID,
72
+ "response_type": "id_token",
73
+ "redirect_uri": REDIRECT,
74
+ "scope": "openid",
75
+ "nonce": secrets.token_urlsafe(8),
76
+ "response_mode": "fragment",
77
+ "prompt": "login", # force a fresh login so you can choose passkey vs password
78
+ }
79
+ url = AUTHORIZE + "?" + urllib.parse.urlencode(params)
80
+
81
+ server = HTTPServer(("localhost", PORT), Handler)
82
+ print("Opening browser… sign in with a PASSKEY (phishing-resistant) or a PASSWORD (not).")
83
+ webbrowser.open(url)
84
+
85
+ # Serve requests until we capture the token (GET / then GET /capture).
86
+ while "id_token" not in _result and "error" not in _result:
87
+ server.handle_request()
88
+
89
+ if "error" in _result:
90
+ print(f"Login error: {_result.get('error')} — {_result.get('error_description', '')}")
91
+ sys.exit(1)
92
+
93
+ idt = _result["id_token"]
94
+ with open(TOKEN_FILE, "w") as f:
95
+ f.write(idt)
96
+ os.chmod(TOKEN_FILE, 0o600)
97
+
98
+ # Decode (unverified) just to tell you which run you'll get.
99
+ payload = idt.split(".")[1]
100
+ payload += "=" * (-len(payload) % 4)
101
+ claims = json.loads(base64.urlsafe_b64decode(payload))
102
+ amr = claims.get("amr") or []
103
+ resistant = any(str(m).lower() in PHISHING_RESISTANT_AMR for m in amr)
104
+
105
+ print(f"\n Signed in as {claims.get('upn') or claims.get('email')}")
106
+ print(f" amr = {amr} -> " +
107
+ ("PHISHING-RESISTANT ✓ (ALLOW run)" if resistant
108
+ else "not phishing-resistant (DENY run)"))
109
+ print(f" token saved to {TOKEN_FILE}")
110
+ print(f"\n export ALICE_TOKEN=$(cat {TOKEN_FILE})\n")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -0,0 +1,88 @@
1
+ # Pydantic AI Examples
2
+
3
+ `SecureAgent` is a drop-in replacement for `pydantic_ai.Agent`. Model calls
4
+ become `tool:<app>/generate` and tool calls become `tool:<app>/<tool_name>`, so
5
+ both are governed by the MAPL policies you already write.
6
+
7
+ ```python
8
+ # BEFORE
9
+ from pydantic_ai import Agent
10
+ agent = Agent(OpenAIChatModel("gpt-4o-mini"), tools=[query_catalog])
11
+
12
+ # AFTER
13
+ from macaw_adapters.pydantic_ai import SecureAgent
14
+ agent = SecureAgent(OpenAIChatModel("gpt-4o-mini"), app_name="pydantic-agent",
15
+ tools=[query_catalog])
16
+ ```
17
+
18
+ ## Setup
19
+
20
+ ```bash
21
+ pip install "macaw-adapters[pydantic-ai]"
22
+ export OPENAI_API_KEY=sk-...
23
+ ```
24
+
25
+ You also need the MACAW LocalAgent running, and the examples' resources allowed
26
+ in your workspace. MAPL is restrict-only: an `intent_policy` can narrow what an
27
+ application may do but never widen it, so the workspace has to grant the
28
+ superset first.
29
+
30
+ Import `policies/app_pydantic-agent.json` via Console > Policies, or add:
31
+
32
+ ```
33
+ tool:pydantic-agent/*
34
+ ```
35
+
36
+ Each example then narrows from there, which is what it is demonstrating.
37
+
38
+ `1d` additionally needs an identity provider with the demo users - see
39
+ `demos/tutorial-1/setup/`.
40
+
41
+ ## Examples
42
+
43
+ | Example | Demonstrates |
44
+ |---------|--------------|
45
+ | `pydanticai_1a_dropin_simple.py` | The import swap. One tool allowed, one denied, model and token ceiling pinned. |
46
+ | `pydanticai_1b_model_routing.py` | `FallbackModel` with policy-driven degradation - a denial on the preferred model falls through to a permitted one. |
47
+ | `pydanticai_1c_native_tools.py` | Provider-side tools (web search) refused by `denied_parameters` before the provider is called. |
48
+ | `pydanticai_1d_multiuser_bind.py` | `bind_to_user()` - one agent, per-user policy for alice and bob. |
49
+ | `pydanticai_1e_mcp_compose.py` | Local tools alongside an MCP server, each governed at its own boundary. |
50
+
51
+ ## What is governed
52
+
53
+ Every way Pydantic AI accepts tools:
54
+
55
+ | | |
56
+ |---|---|
57
+ | `tools=[fn]` | registered with MACAW, dispatched by it |
58
+ | `@agent.tool` / `@agent.tool_plain` | registered with MACAW |
59
+ | `toolsets=[FunctionToolset(...)]` | functions registered with MACAW |
60
+ | `toolsets=[MCPToolset(...)]` | authorized here under the caller's identity; executed by the server, which `SecureMCP` or `SecureMCPProxy` governs |
61
+
62
+ `run()`, `run_sync()`, `run_stream()` and `override()` wrap any per-call model or
63
+ toolsets, so an override cannot route around the adapter. A router such as
64
+ `FallbackModel` has its candidates secured rather than the router itself, so
65
+ policy always sees the model that actually calls a provider.
66
+
67
+ ## Policy
68
+
69
+ Nothing here needs new MAPL. The examples use constraint shapes that already
70
+ ship:
71
+
72
+ ```json
73
+ {
74
+ "resources": ["tool:pydantic-agent/generate", "tool:pydantic-agent/query_catalog"],
75
+ "denied_resources": ["tool:pydantic-agent/export_dataset"],
76
+ "constraints": {
77
+ "parameters": {
78
+ "tool:pydantic-agent/generate": {
79
+ "model": ["gpt-4o-mini"],
80
+ "max_tokens": {"max": 500}
81
+ }
82
+ },
83
+ "denied_parameters": {
84
+ "tool:pydantic-agent/generate": {"native_tools": ["*web_search*"]}
85
+ }
86
+ }
87
+ }
88
+ ```
@@ -0,0 +1,10 @@
1
+ {
2
+ "policy_id": "app:pydantic-agent",
3
+ "description": "Resources used by the Pydantic AI examples. Each example narrows this set with its own intent_policy.",
4
+ "scope": "global",
5
+ "version": "1.0",
6
+ "resources": [
7
+ "tool:pydantic-agent/generate",
8
+ "tool:pydantic-agent/*"
9
+ ]
10
+ }
@@ -0,0 +1,90 @@
1
+ """
2
+ Pydantic AI drop-in: model and tool calls governed by MACAW.
3
+
4
+ The simplest integration path - replace the Agent import and add app_name.
5
+
6
+ # BEFORE
7
+ from pydantic_ai import Agent
8
+ agent = Agent(OpenAIChatModel("gpt-4o-mini"), tools=[query_catalog])
9
+
10
+ # AFTER
11
+ from macaw_adapters.pydantic_ai import SecureAgent
12
+ agent = SecureAgent(OpenAIChatModel("gpt-4o-mini"), app_name="catalog-agent",
13
+ tools=[query_catalog])
14
+
15
+ Every model call becomes tool:catalog-agent/generate and every tool call becomes
16
+ tool:catalog-agent/<tool_name>, so both are governed by MAPL policy.
17
+
18
+ Prerequisites:
19
+ export OPENAI_API_KEY=sk-...
20
+ MACAW LocalAgent running
21
+
22
+ Run:
23
+ python pydanticai_1a_dropin_simple.py
24
+ """
25
+
26
+ import os
27
+ import sys
28
+
29
+ from pydantic_ai.models.openai import OpenAIChatModel
30
+
31
+ from macaw_adapters.pydantic_ai import SecureAgent
32
+
33
+ APP_NAME = "pydantic-agent"
34
+ GENERATE = f"tool:{APP_NAME}/generate"
35
+
36
+
37
+ def query_catalog(table: str, limit: int = 10) -> str:
38
+ """Query the data catalog for rows from a table."""
39
+ print(f" [tool] query_catalog(table={table!r}, limit={limit})")
40
+ return f"{limit} rows from {table}"
41
+
42
+
43
+ def export_dataset(table: str) -> str:
44
+ """Export a full dataset to external storage."""
45
+ print(f" [tool] export_dataset(table={table!r})")
46
+ return f"exported {table}"
47
+
48
+
49
+ def main() -> int:
50
+ if not os.environ.get("OPENAI_API_KEY"):
51
+ print("Set OPENAI_API_KEY first:")
52
+ print(" export OPENAI_API_KEY=sk-...")
53
+ return 1
54
+
55
+ # Policy: gpt-4o-mini only, and the agent may query but not export.
56
+ agent = SecureAgent(
57
+ OpenAIChatModel("gpt-4o-mini"),
58
+ app_name=APP_NAME,
59
+ tools=[query_catalog, export_dataset],
60
+ intent_policy={
61
+ "resources": [GENERATE, f"tool:{APP_NAME}/query_catalog"],
62
+ "denied_resources": [f"tool:{APP_NAME}/export_dataset"],
63
+ "constraints": {
64
+ "parameters": {
65
+ GENERATE: {
66
+ "model": ["gpt-4o-mini"],
67
+ "max_tokens": {"max": 500},
68
+ }
69
+ }
70
+ },
71
+ },
72
+ )
73
+
74
+ print(f"Registered: {agent._macaw.server_id}")
75
+ print(f"Policy: gpt-4o-mini only; query_catalog allowed, export_dataset denied")
76
+
77
+ print("\n--- Allowed: query the catalog ---")
78
+ result = agent.run_sync("Use query_catalog on the customers table with limit 3.")
79
+ print(f" {result.output}")
80
+
81
+ print("\n--- Denied: export a dataset ---")
82
+ result = agent.run_sync("Export the customers table using export_dataset.")
83
+ print(f" {result.output}")
84
+ print("\n The tool never ran. MACAW denied it and the model adapted.")
85
+
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ sys.exit(main())
@@ -0,0 +1,85 @@
1
+ """
2
+ Policy-driven model routing with FallbackModel.
3
+
4
+ Pydantic AI's FallbackModel tries each candidate in turn until one succeeds.
5
+ SecureAgent secures the candidates rather than the router, so policy sees the
6
+ model that would actually call the provider - and a MACAW denial is just another
7
+ reason to move to the next candidate.
8
+
9
+ router = FallbackModel(
10
+ OpenAIChatModel("gpt-4o"), # preferred
11
+ OpenAIChatModel("gpt-4o-mini"), # fallback
12
+ fallback_on=(PermissionDenied,),
13
+ )
14
+
15
+ The same MAPL constraint therefore governs cost tier, data residency, and access
16
+ at once, and degrades instead of failing the run.
17
+
18
+ Prerequisites:
19
+ export OPENAI_API_KEY=sk-...
20
+ MACAW LocalAgent running
21
+ Workspace policy allowing tool:pydantic-agent/*
22
+
23
+ Run:
24
+ python pydanticai_1b_model_routing.py
25
+ """
26
+
27
+ import os
28
+ import sys
29
+
30
+ from macaw_client import PermissionDenied
31
+ from pydantic_ai.models.fallback import FallbackModel
32
+ from pydantic_ai.models.openai import OpenAIChatModel
33
+
34
+ from macaw_adapters.pydantic_ai import SecureAgent
35
+
36
+ APP_NAME = "pydantic-agent"
37
+ GENERATE = f"tool:{APP_NAME}/generate"
38
+
39
+ PREFERRED = "gpt-4o"
40
+ FALLBACK = "gpt-4o-mini"
41
+
42
+
43
+ def run(permitted: str) -> None:
44
+ """Route over both models with policy permitting only `permitted`."""
45
+ router = FallbackModel(
46
+ OpenAIChatModel(PREFERRED),
47
+ OpenAIChatModel(FALLBACK),
48
+ fallback_on=(PermissionDenied,),
49
+ )
50
+
51
+ agent = SecureAgent(
52
+ router,
53
+ app_name=APP_NAME,
54
+ intent_policy={
55
+ "resources": [GENERATE],
56
+ "constraints": {"parameters": {GENERATE: {"model": [permitted]}}},
57
+ },
58
+ )
59
+
60
+ result = agent.run_sync("Say the single word: hello")
61
+ print(f" policy permits : {permitted}")
62
+ print(f" candidates : {[m.wrapped.model_name for m in router.models]}")
63
+ print(f" served by : {result.response.model_name}")
64
+ print(f" output : {result.output}")
65
+
66
+
67
+ def main() -> int:
68
+ if not os.environ.get("OPENAI_API_KEY"):
69
+ print("Set OPENAI_API_KEY first:")
70
+ print(" export OPENAI_API_KEY=sk-...")
71
+ return 1
72
+
73
+ print(f"--- Policy permits the preferred model ({PREFERRED}) ---")
74
+ run(PREFERRED)
75
+
76
+ print(f"\n--- Policy permits only the fallback ({FALLBACK}) ---")
77
+ run(FALLBACK)
78
+ print(f"\n {PREFERRED} was denied by MACAW, so the router moved on.")
79
+ print(" The run succeeded on a permitted model instead of failing.")
80
+
81
+ return 0
82
+
83
+
84
+ if __name__ == "__main__":
85
+ sys.exit(main())
@@ -0,0 +1,83 @@
1
+ """
2
+ Governing provider-side (native) tools.
3
+
4
+ Native tools such as web search and code execution run at the model provider,
5
+ not in your process, so they never reach a toolset and cannot be intercepted
6
+ after the fact. SecureAgent reports the native tools offered on each request as
7
+ a parameter of tool:<app>/generate, so MAPL can refuse the call before it is
8
+ made:
9
+
10
+ "constraints": {
11
+ "denied_parameters": {
12
+ "tool:pydantic-agent/generate": {"native_tools": ["*web_search*"]}
13
+ }
14
+ }
15
+
16
+ The denial happens before the provider is contacted, so nothing is searched.
17
+
18
+ Prerequisites:
19
+ export OPENAI_API_KEY=sk-...
20
+ MACAW LocalAgent running
21
+ Workspace policy allowing tool:pydantic-agent/*
22
+
23
+ Run:
24
+ python pydanticai_1c_native_tools.py
25
+ """
26
+
27
+ import os
28
+ import sys
29
+
30
+ from macaw_client import PermissionDenied
31
+ from pydantic_ai.capabilities import NativeTool
32
+ from pydantic_ai.models.openai import OpenAIResponsesModel
33
+ from pydantic_ai.native_tools import WebSearchTool
34
+
35
+ from macaw_adapters.pydantic_ai import SecureAgent
36
+
37
+ APP_NAME = "pydantic-agent"
38
+ GENERATE = f"tool:{APP_NAME}/generate"
39
+
40
+ ALLOW = {"resources": [GENERATE]}
41
+ DENY_WEB_SEARCH = {
42
+ "resources": [GENERATE],
43
+ "constraints": {
44
+ "denied_parameters": {GENERATE: {"native_tools": ["*web_search*"]}}
45
+ },
46
+ }
47
+
48
+
49
+ def run(label: str, policy: dict, capabilities: list) -> None:
50
+ agent = SecureAgent(
51
+ OpenAIResponsesModel("gpt-4o-mini"),
52
+ app_name=APP_NAME,
53
+ intent_policy=policy,
54
+ capabilities=capabilities,
55
+ )
56
+ print(f"\n--- {label} ---")
57
+ try:
58
+ result = agent.run_sync("What is the capital of France? One word.")
59
+ print(f" allowed: {result.output}")
60
+ except PermissionDenied as e:
61
+ print(f" denied by MACAW: {e}")
62
+ print(" The provider was never called, so nothing was searched.")
63
+
64
+
65
+ def main() -> int:
66
+ if not os.environ.get("OPENAI_API_KEY"):
67
+ print("Set OPENAI_API_KEY first:")
68
+ print(" export OPENAI_API_KEY=sk-...")
69
+ return 1
70
+
71
+ run("No native tools offered; policy denies web_search", DENY_WEB_SEARCH, [])
72
+ run(
73
+ "web_search offered; policy denies it",
74
+ DENY_WEB_SEARCH,
75
+ [NativeTool(WebSearchTool())],
76
+ )
77
+ run("web_search offered; policy permits it", ALLOW, [NativeTool(WebSearchTool())])
78
+
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ sys.exit(main())