@macaw-ai/client 0.0.0-stage → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +339 -0
- package/bin/install.js +278 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,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())
|