acr-sdk 0.1.0__tar.gz
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.
- acr_sdk-0.1.0/.gitignore +8 -0
- acr_sdk-0.1.0/PKG-INFO +229 -0
- acr_sdk-0.1.0/README.md +193 -0
- acr_sdk-0.1.0/acr/__init__.py +55 -0
- acr_sdk-0.1.0/acr/_jwt.py +64 -0
- acr_sdk-0.1.0/acr/client.py +639 -0
- acr_sdk-0.1.0/acr/dsl.py +329 -0
- acr_sdk-0.1.0/acr/exceptions.py +41 -0
- acr_sdk-0.1.0/acr/langchain.py +33 -0
- acr_sdk-0.1.0/acr/local.py +640 -0
- acr_sdk-0.1.0/acr/models.py +245 -0
- acr_sdk-0.1.0/examples/demo_wow.py +165 -0
- acr_sdk-0.1.0/examples/e2e_gateway.py +72 -0
- acr_sdk-0.1.0/examples/quickstart.py +76 -0
- acr_sdk-0.1.0/pyproject.toml +73 -0
- acr_sdk-0.1.0/tests/__init__.py +0 -0
- acr_sdk-0.1.0/tests/test_client.py +470 -0
- acr_sdk-0.1.0/tests/test_dsl.py +152 -0
- acr_sdk-0.1.0/tests/test_e2e_gateway.py +62 -0
- acr_sdk-0.1.0/tests/test_local.py +274 -0
- acr_sdk-0.1.0/tests/test_models.py +111 -0
acr_sdk-0.1.0/.gitignore
ADDED
acr_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acr-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Agent Capability Runtime — runtime-enforced capability permissions for AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/agent-capability-runtime/Agent_Capability_Runtime
|
|
6
|
+
Project-URL: Documentation, https://github.com/agent-capability-runtime/Agent_Capability_Runtime/tree/main/packages/sdk-python
|
|
7
|
+
Project-URL: Repository, https://github.com/agent-capability-runtime/Agent_Capability_Runtime
|
|
8
|
+
Project-URL: Issues, https://github.com/agent-capability-runtime/Agent_Capability_Runtime/issues
|
|
9
|
+
Author: Agent Capability Runtime Contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai-agents,authorization,capability-token,governance,runtime
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
33
|
+
Provides-Extra: langchain
|
|
34
|
+
Requires-Dist: acr-langchain>=0.1.0; extra == 'langchain'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# ACR Python SDK
|
|
38
|
+
|
|
39
|
+
**Python client for [Agent Capability Runtime](https://github.com/agent-capability-runtime/Agent_Capability_Runtime)** — runtime-enforced capability permissions for AI agents.
|
|
40
|
+
|
|
41
|
+
[](https://www.python.org/)
|
|
42
|
+
[](../../LICENSE)
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
From source (PyPI publish pending):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd Agent_Capability_Runtime/packages/sdk-python
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
When published:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install acr-sdk
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### Embedded mode — zero infrastructure
|
|
62
|
+
|
|
63
|
+
No gateway, no Docker, no Node. Policy enforcement runs inside your process:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from acr import LocalAcrClient, can
|
|
67
|
+
|
|
68
|
+
client = LocalAcrClient() # in-process runtime
|
|
69
|
+
|
|
70
|
+
grant = client.grant_sync(
|
|
71
|
+
can("gmail.send").only_domain("company.com").limit(5).to_grant_input(agent_id="a1")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
result = client.execute_sync(
|
|
75
|
+
token=grant.token, tool="gmail.send",
|
|
76
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
77
|
+
)
|
|
78
|
+
print(result.decision) # "ALLOW"
|
|
79
|
+
|
|
80
|
+
result = client.execute_sync(
|
|
81
|
+
token=grant.token, tool="gmail.send",
|
|
82
|
+
payload={"to": "attacker@gmail.com", "subject": "Exfil"},
|
|
83
|
+
)
|
|
84
|
+
print(result.decision) # "DENY"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or let the environment decide (gateway when `ACR_GATEWAY_URL` is set, embedded otherwise):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from acr import create_client
|
|
91
|
+
|
|
92
|
+
client = create_client()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Async (FastAPI / LangChain) — gateway mode
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from acr import AcrClient, can
|
|
99
|
+
|
|
100
|
+
async with AcrClient(base_url="http://localhost:3000") as client:
|
|
101
|
+
# Grant a scoped capability
|
|
102
|
+
grant = await client.grant(
|
|
103
|
+
can("gmail.send")
|
|
104
|
+
.only_domain("company.com")
|
|
105
|
+
.limit(5)
|
|
106
|
+
.expires_in("10m")
|
|
107
|
+
.to_grant_input(agent_id="support_agent")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Execute — ALLOW (internal domain)
|
|
111
|
+
result = await client.execute(
|
|
112
|
+
token=grant.token,
|
|
113
|
+
tool="gmail.send",
|
|
114
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
115
|
+
)
|
|
116
|
+
print(result.decision) # "ALLOW"
|
|
117
|
+
|
|
118
|
+
# Execute — DENY (external domain blocked)
|
|
119
|
+
result = await client.execute(
|
|
120
|
+
token=grant.token,
|
|
121
|
+
tool="gmail.send",
|
|
122
|
+
payload={"to": "attacker@gmail.com", "subject": "Exfil"},
|
|
123
|
+
)
|
|
124
|
+
print(result.decision) # "DENY"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Sync
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from acr import AcrClient, can
|
|
131
|
+
|
|
132
|
+
client = AcrClient(base_url="http://localhost:3000")
|
|
133
|
+
|
|
134
|
+
grant = client.grant_sync(
|
|
135
|
+
can("gmail.send")
|
|
136
|
+
.only_domain("company.com")
|
|
137
|
+
.to_grant_input(agent_id="agent_1")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result = client.execute_sync(
|
|
141
|
+
token=grant.token,
|
|
142
|
+
tool="gmail.send",
|
|
143
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
144
|
+
)
|
|
145
|
+
client.close()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Fluent DSL
|
|
149
|
+
|
|
150
|
+
The `can()` builder mirrors the TypeScript DSL:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from acr import can
|
|
154
|
+
|
|
155
|
+
# Email constraints
|
|
156
|
+
can("gmail.send").only_domain("company.com").limit(5).no_attachments()
|
|
157
|
+
|
|
158
|
+
# HTTP constraints
|
|
159
|
+
can("http.request").where(method.in_(["GET", "POST"])).where(url.in_(["https://api.example.com"]))
|
|
160
|
+
|
|
161
|
+
# Spending limit with approval
|
|
162
|
+
can("gmail.send").max_spend(100_00).require_approval()
|
|
163
|
+
|
|
164
|
+
# Intent-based governance
|
|
165
|
+
can("gmail.send").when_intent("customer_support").when_intent_action("support", "reply")
|
|
166
|
+
|
|
167
|
+
# Time-based
|
|
168
|
+
can("gmail.send").allowed_hours(9, 17)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Full API
|
|
172
|
+
|
|
173
|
+
| Method | Async | Sync |
|
|
174
|
+
|--------|-------|------|
|
|
175
|
+
| Grant capability | `client.grant(input)` | `client.grant_sync(input)` |
|
|
176
|
+
| Execute tool | `client.execute(...)` | `client.execute_sync(...)` |
|
|
177
|
+
| Delegate capability | `client.delegate(parent_token, input)` | `client.delegate_sync(...)` |
|
|
178
|
+
| Revoke capability | `client.revoke(capability_id)` | `client.revoke_sync(...)` |
|
|
179
|
+
| List approvals | `client.list_approvals()` | `client.list_approvals_sync()` |
|
|
180
|
+
| Approve | `client.approve(approval_id)` | `client.approve_sync(...)` |
|
|
181
|
+
| Reject | `client.reject(approval_id)` | `client.reject_sync(...)` |
|
|
182
|
+
| Audit log | `client.list_audit()` | `client.list_audit_sync()` |
|
|
183
|
+
| Verify audit chain | `client.verify_audit_chain()` | `client.verify_audit_chain_sync()` |
|
|
184
|
+
| Health check | `client.health()` | `client.health_sync()` |
|
|
185
|
+
|
|
186
|
+
## Admin Authentication
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
client = AcrClient(
|
|
190
|
+
base_url="http://localhost:3000",
|
|
191
|
+
admin_api_key="your-admin-secret",
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## LangChain integration
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install "acr-sdk[langchain]"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from acr import can
|
|
203
|
+
from acr.langchain import protect
|
|
204
|
+
|
|
205
|
+
tools = protect(my_tools, agent_id="my_agent", policy=can("http.request").limit(50))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See [packages/integrations/langchain](../integrations/langchain).
|
|
209
|
+
|
|
210
|
+
## Requirements
|
|
211
|
+
|
|
212
|
+
- Python 3.10+
|
|
213
|
+
- Embedded mode: nothing else
|
|
214
|
+
- Gateway mode: a running ACR gateway (`pnpm dev:gateway`)
|
|
215
|
+
|
|
216
|
+
## Gateway e2e
|
|
217
|
+
|
|
218
|
+
With the gateway running:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
python packages/sdk-python/examples/demo_wow.py # deny / approval / revoke narrative
|
|
222
|
+
python packages/sdk-python/examples/e2e_gateway.py
|
|
223
|
+
# or
|
|
224
|
+
ACR_RUN_E2E=1 pytest packages/sdk-python/tests/test_e2e_gateway.py -v
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT — see [LICENSE](../../LICENSE)
|
acr_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# ACR Python SDK
|
|
2
|
+
|
|
3
|
+
**Python client for [Agent Capability Runtime](https://github.com/agent-capability-runtime/Agent_Capability_Runtime)** — runtime-enforced capability permissions for AI agents.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
From source (PyPI publish pending):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd Agent_Capability_Runtime/packages/sdk-python
|
|
14
|
+
pip install -e ".[dev]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
When published:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install acr-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Embedded mode — zero infrastructure
|
|
26
|
+
|
|
27
|
+
No gateway, no Docker, no Node. Policy enforcement runs inside your process:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from acr import LocalAcrClient, can
|
|
31
|
+
|
|
32
|
+
client = LocalAcrClient() # in-process runtime
|
|
33
|
+
|
|
34
|
+
grant = client.grant_sync(
|
|
35
|
+
can("gmail.send").only_domain("company.com").limit(5).to_grant_input(agent_id="a1")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
result = client.execute_sync(
|
|
39
|
+
token=grant.token, tool="gmail.send",
|
|
40
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
41
|
+
)
|
|
42
|
+
print(result.decision) # "ALLOW"
|
|
43
|
+
|
|
44
|
+
result = client.execute_sync(
|
|
45
|
+
token=grant.token, tool="gmail.send",
|
|
46
|
+
payload={"to": "attacker@gmail.com", "subject": "Exfil"},
|
|
47
|
+
)
|
|
48
|
+
print(result.decision) # "DENY"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or let the environment decide (gateway when `ACR_GATEWAY_URL` is set, embedded otherwise):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from acr import create_client
|
|
55
|
+
|
|
56
|
+
client = create_client()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Async (FastAPI / LangChain) — gateway mode
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from acr import AcrClient, can
|
|
63
|
+
|
|
64
|
+
async with AcrClient(base_url="http://localhost:3000") as client:
|
|
65
|
+
# Grant a scoped capability
|
|
66
|
+
grant = await client.grant(
|
|
67
|
+
can("gmail.send")
|
|
68
|
+
.only_domain("company.com")
|
|
69
|
+
.limit(5)
|
|
70
|
+
.expires_in("10m")
|
|
71
|
+
.to_grant_input(agent_id="support_agent")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Execute — ALLOW (internal domain)
|
|
75
|
+
result = await client.execute(
|
|
76
|
+
token=grant.token,
|
|
77
|
+
tool="gmail.send",
|
|
78
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
79
|
+
)
|
|
80
|
+
print(result.decision) # "ALLOW"
|
|
81
|
+
|
|
82
|
+
# Execute — DENY (external domain blocked)
|
|
83
|
+
result = await client.execute(
|
|
84
|
+
token=grant.token,
|
|
85
|
+
tool="gmail.send",
|
|
86
|
+
payload={"to": "attacker@gmail.com", "subject": "Exfil"},
|
|
87
|
+
)
|
|
88
|
+
print(result.decision) # "DENY"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Sync
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from acr import AcrClient, can
|
|
95
|
+
|
|
96
|
+
client = AcrClient(base_url="http://localhost:3000")
|
|
97
|
+
|
|
98
|
+
grant = client.grant_sync(
|
|
99
|
+
can("gmail.send")
|
|
100
|
+
.only_domain("company.com")
|
|
101
|
+
.to_grant_input(agent_id="agent_1")
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = client.execute_sync(
|
|
105
|
+
token=grant.token,
|
|
106
|
+
tool="gmail.send",
|
|
107
|
+
payload={"to": "user@company.com", "subject": "Hello"},
|
|
108
|
+
)
|
|
109
|
+
client.close()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Fluent DSL
|
|
113
|
+
|
|
114
|
+
The `can()` builder mirrors the TypeScript DSL:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from acr import can
|
|
118
|
+
|
|
119
|
+
# Email constraints
|
|
120
|
+
can("gmail.send").only_domain("company.com").limit(5).no_attachments()
|
|
121
|
+
|
|
122
|
+
# HTTP constraints
|
|
123
|
+
can("http.request").where(method.in_(["GET", "POST"])).where(url.in_(["https://api.example.com"]))
|
|
124
|
+
|
|
125
|
+
# Spending limit with approval
|
|
126
|
+
can("gmail.send").max_spend(100_00).require_approval()
|
|
127
|
+
|
|
128
|
+
# Intent-based governance
|
|
129
|
+
can("gmail.send").when_intent("customer_support").when_intent_action("support", "reply")
|
|
130
|
+
|
|
131
|
+
# Time-based
|
|
132
|
+
can("gmail.send").allowed_hours(9, 17)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Full API
|
|
136
|
+
|
|
137
|
+
| Method | Async | Sync |
|
|
138
|
+
|--------|-------|------|
|
|
139
|
+
| Grant capability | `client.grant(input)` | `client.grant_sync(input)` |
|
|
140
|
+
| Execute tool | `client.execute(...)` | `client.execute_sync(...)` |
|
|
141
|
+
| Delegate capability | `client.delegate(parent_token, input)` | `client.delegate_sync(...)` |
|
|
142
|
+
| Revoke capability | `client.revoke(capability_id)` | `client.revoke_sync(...)` |
|
|
143
|
+
| List approvals | `client.list_approvals()` | `client.list_approvals_sync()` |
|
|
144
|
+
| Approve | `client.approve(approval_id)` | `client.approve_sync(...)` |
|
|
145
|
+
| Reject | `client.reject(approval_id)` | `client.reject_sync(...)` |
|
|
146
|
+
| Audit log | `client.list_audit()` | `client.list_audit_sync()` |
|
|
147
|
+
| Verify audit chain | `client.verify_audit_chain()` | `client.verify_audit_chain_sync()` |
|
|
148
|
+
| Health check | `client.health()` | `client.health_sync()` |
|
|
149
|
+
|
|
150
|
+
## Admin Authentication
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
client = AcrClient(
|
|
154
|
+
base_url="http://localhost:3000",
|
|
155
|
+
admin_api_key="your-admin-secret",
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## LangChain integration
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install "acr-sdk[langchain]"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from acr import can
|
|
167
|
+
from acr.langchain import protect
|
|
168
|
+
|
|
169
|
+
tools = protect(my_tools, agent_id="my_agent", policy=can("http.request").limit(50))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
See [packages/integrations/langchain](../integrations/langchain).
|
|
173
|
+
|
|
174
|
+
## Requirements
|
|
175
|
+
|
|
176
|
+
- Python 3.10+
|
|
177
|
+
- Embedded mode: nothing else
|
|
178
|
+
- Gateway mode: a running ACR gateway (`pnpm dev:gateway`)
|
|
179
|
+
|
|
180
|
+
## Gateway e2e
|
|
181
|
+
|
|
182
|
+
With the gateway running:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python packages/sdk-python/examples/demo_wow.py # deny / approval / revoke narrative
|
|
186
|
+
python packages/sdk-python/examples/e2e_gateway.py
|
|
187
|
+
# or
|
|
188
|
+
ACR_RUN_E2E=1 pytest packages/sdk-python/tests/test_e2e_gateway.py -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT — see [LICENSE](../../LICENSE)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ACR SDK — Python client for Agent Capability Runtime."""
|
|
2
|
+
|
|
3
|
+
from acr.client import AcrClient
|
|
4
|
+
from acr.dsl import can, domain, method, url, hours, intent, PolicyBuilder
|
|
5
|
+
from acr.exceptions import AcrError, GrantError, ExecuteError, ApprovalError
|
|
6
|
+
from acr.local import LocalAcrClient, create_client
|
|
7
|
+
from acr.models import (
|
|
8
|
+
ConstraintSet,
|
|
9
|
+
ExecuteInput,
|
|
10
|
+
ExecuteResult,
|
|
11
|
+
ExecuteSuccess,
|
|
12
|
+
ExecuteDenied,
|
|
13
|
+
ExecuteApprovalRequired,
|
|
14
|
+
ExecuteSimulated,
|
|
15
|
+
GrantCapabilityInput,
|
|
16
|
+
GrantResponse,
|
|
17
|
+
ApprovalRequest,
|
|
18
|
+
AuditEvent,
|
|
19
|
+
DelegateCapabilityInput,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Client
|
|
26
|
+
"AcrClient",
|
|
27
|
+
"LocalAcrClient",
|
|
28
|
+
"create_client",
|
|
29
|
+
# DSL
|
|
30
|
+
"can",
|
|
31
|
+
"domain",
|
|
32
|
+
"method",
|
|
33
|
+
"url",
|
|
34
|
+
"hours",
|
|
35
|
+
"intent",
|
|
36
|
+
"PolicyBuilder",
|
|
37
|
+
# Exceptions
|
|
38
|
+
"AcrError",
|
|
39
|
+
"GrantError",
|
|
40
|
+
"ExecuteError",
|
|
41
|
+
"ApprovalError",
|
|
42
|
+
# Models
|
|
43
|
+
"ConstraintSet",
|
|
44
|
+
"ExecuteInput",
|
|
45
|
+
"ExecuteResult",
|
|
46
|
+
"ExecuteSuccess",
|
|
47
|
+
"ExecuteDenied",
|
|
48
|
+
"ExecuteApprovalRequired",
|
|
49
|
+
"ExecuteSimulated",
|
|
50
|
+
"GrantCapabilityInput",
|
|
51
|
+
"GrantResponse",
|
|
52
|
+
"ApprovalRequest",
|
|
53
|
+
"AuditEvent",
|
|
54
|
+
"DelegateCapabilityInput",
|
|
55
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Minimal HS256 JWT encode/decode using only the standard library.
|
|
2
|
+
|
|
3
|
+
Used by the embedded local runtime (``acr.local``). Tokens are interoperable
|
|
4
|
+
with the gateway's HS256 signing when the same secret is configured.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _b64url_encode(data: bytes) -> str:
|
|
17
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _b64url_decode(data: str) -> bytes:
|
|
21
|
+
padding = "=" * (-len(data) % 4)
|
|
22
|
+
return base64.urlsafe_b64decode(data + padding)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def encode_hs256(payload: dict[str, Any], secret: str) -> str:
|
|
26
|
+
"""Sign a JWT payload with HS256."""
|
|
27
|
+
header = {"alg": "HS256", "typ": "JWT"}
|
|
28
|
+
header_b64 = _b64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
29
|
+
payload_b64 = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
|
30
|
+
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
31
|
+
signature = hmac.new(secret.encode("utf-8"), signing_input, hashlib.sha256).digest()
|
|
32
|
+
return f"{header_b64}.{payload_b64}.{_b64url_encode(signature)}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decode_hs256(token: str, secret: str) -> dict[str, Any]:
|
|
36
|
+
"""Verify an HS256 JWT signature and return the payload.
|
|
37
|
+
|
|
38
|
+
Raises ValueError for malformed tokens or signature mismatch.
|
|
39
|
+
Expiry is NOT checked here — callers decide how to surface it.
|
|
40
|
+
"""
|
|
41
|
+
parts = token.split(".")
|
|
42
|
+
if len(parts) != 3:
|
|
43
|
+
raise ValueError("malformed token")
|
|
44
|
+
|
|
45
|
+
header_b64, payload_b64, signature_b64 = parts
|
|
46
|
+
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
|
|
47
|
+
expected = hmac.new(secret.encode("utf-8"), signing_input, hashlib.sha256).digest()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
actual = _b64url_decode(signature_b64)
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
raise ValueError("malformed signature") from exc
|
|
53
|
+
|
|
54
|
+
if not hmac.compare_digest(expected, actual):
|
|
55
|
+
raise ValueError("invalid signature")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
payload = json.loads(_b64url_decode(payload_b64))
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise ValueError("malformed payload") from exc
|
|
61
|
+
|
|
62
|
+
if not isinstance(payload, dict):
|
|
63
|
+
raise ValueError("invalid payload")
|
|
64
|
+
return payload
|