modus-sdk 1.1.2__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.
Potentially problematic release.
This version of modus-sdk might be problematic. Click here for more details.
- modus_sdk-1.1.2/.gitignore +10 -0
- modus_sdk-1.1.2/PKG-INFO +296 -0
- modus_sdk-1.1.2/README.md +265 -0
- modus_sdk-1.1.2/pyproject.toml +58 -0
- modus_sdk-1.1.2/src/modus/__init__.py +117 -0
- modus_sdk-1.1.2/src/modus/_http.py +79 -0
- modus_sdk-1.1.2/src/modus/async_client.py +604 -0
- modus_sdk-1.1.2/src/modus/client.py +604 -0
- modus_sdk-1.1.2/src/modus/crypto.py +77 -0
- modus_sdk-1.1.2/src/modus/exceptions.py +52 -0
- modus_sdk-1.1.2/src/modus/py.typed +0 -0
- modus_sdk-1.1.2/src/modus/types.py +446 -0
- modus_sdk-1.1.2/tests/__init__.py +0 -0
- modus_sdk-1.1.2/tests/conftest.py +30 -0
- modus_sdk-1.1.2/tests/test_async_client.py +984 -0
- modus_sdk-1.1.2/tests/test_sync_client.py +1019 -0
modus_sdk-1.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modus-sdk
|
|
3
|
+
Version: 1.1.2
|
|
4
|
+
Summary: Python SDK for the Modus REST API — manage agent passports, gates, and enforcement policies.
|
|
5
|
+
Project-URL: Homepage, https://modusoperator.com
|
|
6
|
+
Project-URL: Documentation, https://modusoperator.com/guide
|
|
7
|
+
Project-URL: Repository, https://github.com/StandardLogic/modus-mcp
|
|
8
|
+
Author-email: "Standard Logic Co." <support@modusoperator.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-agents,mcp,modus,passports,permissions,trust
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: canonicaljson>=2.0.0
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: pynacl>=1.5.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# modus-sdk
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/modus-sdk/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Python SDK for the [Modus](https://modusoperator.com) REST API. Manage agent passports, gates, and enforcement policies programmatically.
|
|
38
|
+
|
|
39
|
+
The MCP server (for Claude Desktop, Cursor, etc.) is the separate TypeScript package: [modus-mcp on npm](https://www.npmjs.com/package/modus-mcp).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install modus-sdk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from modus import ModusClient
|
|
55
|
+
|
|
56
|
+
client = ModusClient(api_key="mod_live_xxx")
|
|
57
|
+
|
|
58
|
+
# List all gates
|
|
59
|
+
gates = client.list_gates()
|
|
60
|
+
|
|
61
|
+
# Create a gate
|
|
62
|
+
gate = client.create_gate(name="My API", gate_id="gate_my-api")
|
|
63
|
+
|
|
64
|
+
# Issue a passport
|
|
65
|
+
passport = client.issue_passport(
|
|
66
|
+
"gate_my-api",
|
|
67
|
+
agent_id="agent-001",
|
|
68
|
+
permissions=["read", "write"],
|
|
69
|
+
expires_in="7d",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check authorization
|
|
73
|
+
result = client.check_gate("gate_my-api", action="read", passport_id=passport["passport_id"])
|
|
74
|
+
print(result["allowed"]) # True
|
|
75
|
+
|
|
76
|
+
# Enforce with constraints
|
|
77
|
+
enforcement = client.enforce_action(
|
|
78
|
+
passport_id=passport["passport_id"],
|
|
79
|
+
action="write",
|
|
80
|
+
cost_cents=500,
|
|
81
|
+
)
|
|
82
|
+
print(enforcement["decision"]) # "PERMIT"
|
|
83
|
+
|
|
84
|
+
client.close()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Async Usage
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from modus import AsyncModusClient
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
async with AsyncModusClient(api_key="mod_live_xxx") as client:
|
|
97
|
+
gates = await client.list_gates()
|
|
98
|
+
for gate in gates:
|
|
99
|
+
passports = await client.list_passports(gate["gate_id"])
|
|
100
|
+
print(f"{gate['name']}: {len(passports)} passports")
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Both clients support context managers for automatic cleanup:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
with ModusClient(api_key="mod_live_xxx") as client:
|
|
109
|
+
gates = client.list_gates()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## API Coverage
|
|
115
|
+
|
|
116
|
+
### Gates
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
client.list_gates()
|
|
120
|
+
client.get_gate("gate_my-api")
|
|
121
|
+
client.create_gate(name="My API", gate_id="gate_my-api")
|
|
122
|
+
client.update_gate("gate_my-api", name="Updated Name")
|
|
123
|
+
client.delete_gate("gate_my-api")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Passports
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
client.list_passports("gate_my-api")
|
|
130
|
+
client.get_passport("gate_my-api", "pp_xxx")
|
|
131
|
+
client.issue_passport("gate_my-api", agent_id="agent-001", permissions=["read"])
|
|
132
|
+
client.revoke_passport("gate_my-api", "pp_xxx")
|
|
133
|
+
client.reissue_passport("pp_xxx", accept_catalog_version=2)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Attestations
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
client.list_attestations("gate_my-api", limit=50)
|
|
140
|
+
client.record_attestation("gate_my-api", passport_id="pp_xxx", permission="read", tool_name="search", result="allowed")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Permission Catalog
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
client.get_catalog("gate_my-api")
|
|
147
|
+
client.create_catalog("gate_my-api", permissions=[...])
|
|
148
|
+
client.publish_catalog("gate_my-api", change_summary="Added search permission")
|
|
149
|
+
client.list_catalog_versions("gate_my-api")
|
|
150
|
+
client.get_catalog_version("gate_my-api", version=1)
|
|
151
|
+
client.get_catalog_impact("gate_my-api")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Gate Check
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
client.check_gate("gate_my-api", action="read", passport_id="pp_xxx")
|
|
158
|
+
client.authorize_dry_run(gate_id="gate_my-api", passport={"..."}, requested_permission="read")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Constraints
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
client.get_constraints("pp_xxx")
|
|
165
|
+
client.set_constraints("pp_xxx", {"read": {"core:rate:max_per_minute": 100}})
|
|
166
|
+
client.list_constraint_types(category="cost")
|
|
167
|
+
client.list_constraint_templates(category="security")
|
|
168
|
+
client.apply_constraint_template("pp_xxx", "conservative-agent")
|
|
169
|
+
client.create_constraint_template(slug="my-template", name="My Template", constraints={...})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Enforcement (CEL)
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
client.enforce_action(passport_id="pp_xxx", action="write", cost_cents=500)
|
|
176
|
+
client.list_enforcement_attestations("pp_xxx", decision="BLOCK")
|
|
177
|
+
client.get_enforcement_attestation("enf_xxx")
|
|
178
|
+
client.verify_enforcement_attestation("enf_xxx")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Anonymous Access
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
client.get_anonymous_policy("gate_my-api")
|
|
185
|
+
client.set_anonymous_policy("gate_my-api", enabled=True, allowed_actions=["read"])
|
|
186
|
+
client.get_anonymous_log("gate_my-api")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Commerce
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
client.discover_services("flights:search", max_price_cents=100, sort="price_asc")
|
|
193
|
+
client.issue_consumption_attestation(passport_id="pp_xxx", gate_id="gate_my-api", action="flights:search", outcome="success")
|
|
194
|
+
client.generate_settlement(gate_id="gate_my-api", period_type="monthly", period_start="2025-01-01", period_end="2025-01-31")
|
|
195
|
+
client.list_settlements(gate_id="gate_my-api", status="pending")
|
|
196
|
+
client.get_settlement("stl_xxx")
|
|
197
|
+
client.update_settlement_status("stl_xxx", "invoiced")
|
|
198
|
+
client.get_sla_compliance("gate_my-api", period_start="2025-01-01", period_end="2025-01-31")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Cumulative State
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
client.get_cumulative_state("pp_xxx")
|
|
205
|
+
client.reset_cumulative_state("pp_xxx", window_type="daily")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### API Keys
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
client.list_api_keys()
|
|
212
|
+
client.create_api_key(name="My Key", scopes=["gates:read", "passports:write"])
|
|
213
|
+
client.revoke_api_key("key_xxx")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Cryptographic Verification
|
|
219
|
+
|
|
220
|
+
Local verification utilities for Ed25519 signatures and RFC 8785 content hashing:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from modus import verify_attestation_signature, verify_content_hash, compute_content_hash
|
|
224
|
+
|
|
225
|
+
valid = verify_attestation_signature(
|
|
226
|
+
attestation_json='{"gate_id":"gate_my-api",...}',
|
|
227
|
+
signature_b64="base64-sig...",
|
|
228
|
+
public_key_b64="base64-key...",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
matches = verify_content_hash(catalog_snapshot, expected_hash)
|
|
232
|
+
hash_hex = compute_content_hash({"key": "value"})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Error Handling
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from modus import (
|
|
241
|
+
ModusError,
|
|
242
|
+
AuthenticationError,
|
|
243
|
+
AuthorizationError,
|
|
244
|
+
NotFoundError,
|
|
245
|
+
RateLimitError,
|
|
246
|
+
ValidationError,
|
|
247
|
+
ConflictError,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
client.get_gate("gate_nonexistent")
|
|
252
|
+
except NotFoundError as e:
|
|
253
|
+
print(f"Not found: {e}")
|
|
254
|
+
print(f"Status: {e.status_code}") # 404
|
|
255
|
+
except RateLimitError as e:
|
|
256
|
+
print(f"Rate limited, retry after: {e.retry_after}s")
|
|
257
|
+
except ModusError as e:
|
|
258
|
+
print(f"API error: {e} (HTTP {e.status_code})")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
| Exception | HTTP Status |
|
|
262
|
+
|-----------|-------------|
|
|
263
|
+
| `AuthenticationError` | 401 |
|
|
264
|
+
| `AuthorizationError` | 403 |
|
|
265
|
+
| `NotFoundError` | 404 |
|
|
266
|
+
| `ConflictError` | 409 |
|
|
267
|
+
| `ValidationError` | 400, 422 |
|
|
268
|
+
| `RateLimitError` | 429 |
|
|
269
|
+
| `ModusError` | All other errors |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Environment Variables
|
|
274
|
+
|
|
275
|
+
| Variable | Required | Default | Description |
|
|
276
|
+
|----------|----------|---------|-------------|
|
|
277
|
+
| `MODUS_API_KEY` | Yes | — | Your Modus API key |
|
|
278
|
+
| `MODUS_API_URL` | No | `https://modusoperator.com` | API base URL (override for local dev) |
|
|
279
|
+
|
|
280
|
+
You can also pass these directly to the client:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
client = ModusClient(api_key="mod_live_xxx", base_url="http://localhost:3000")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Documentation
|
|
289
|
+
|
|
290
|
+
Full documentation at [modusoperator.com/docs](https://modusoperator.com/docs)
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT — [Standard Logic Co.](https://standardlogic.ai)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# modus-sdk
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/modus-sdk/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Python SDK for the [Modus](https://modusoperator.com) REST API. Manage agent passports, gates, and enforcement policies programmatically.
|
|
7
|
+
|
|
8
|
+
The MCP server (for Claude Desktop, Cursor, etc.) is the separate TypeScript package: [modus-mcp on npm](https://www.npmjs.com/package/modus-mcp).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install modus-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from modus import ModusClient
|
|
24
|
+
|
|
25
|
+
client = ModusClient(api_key="mod_live_xxx")
|
|
26
|
+
|
|
27
|
+
# List all gates
|
|
28
|
+
gates = client.list_gates()
|
|
29
|
+
|
|
30
|
+
# Create a gate
|
|
31
|
+
gate = client.create_gate(name="My API", gate_id="gate_my-api")
|
|
32
|
+
|
|
33
|
+
# Issue a passport
|
|
34
|
+
passport = client.issue_passport(
|
|
35
|
+
"gate_my-api",
|
|
36
|
+
agent_id="agent-001",
|
|
37
|
+
permissions=["read", "write"],
|
|
38
|
+
expires_in="7d",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Check authorization
|
|
42
|
+
result = client.check_gate("gate_my-api", action="read", passport_id=passport["passport_id"])
|
|
43
|
+
print(result["allowed"]) # True
|
|
44
|
+
|
|
45
|
+
# Enforce with constraints
|
|
46
|
+
enforcement = client.enforce_action(
|
|
47
|
+
passport_id=passport["passport_id"],
|
|
48
|
+
action="write",
|
|
49
|
+
cost_cents=500,
|
|
50
|
+
)
|
|
51
|
+
print(enforcement["decision"]) # "PERMIT"
|
|
52
|
+
|
|
53
|
+
client.close()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Async Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from modus import AsyncModusClient
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with AsyncModusClient(api_key="mod_live_xxx") as client:
|
|
66
|
+
gates = await client.list_gates()
|
|
67
|
+
for gate in gates:
|
|
68
|
+
passports = await client.list_passports(gate["gate_id"])
|
|
69
|
+
print(f"{gate['name']}: {len(passports)} passports")
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Both clients support context managers for automatic cleanup:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
with ModusClient(api_key="mod_live_xxx") as client:
|
|
78
|
+
gates = client.list_gates()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## API Coverage
|
|
84
|
+
|
|
85
|
+
### Gates
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
client.list_gates()
|
|
89
|
+
client.get_gate("gate_my-api")
|
|
90
|
+
client.create_gate(name="My API", gate_id="gate_my-api")
|
|
91
|
+
client.update_gate("gate_my-api", name="Updated Name")
|
|
92
|
+
client.delete_gate("gate_my-api")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Passports
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
client.list_passports("gate_my-api")
|
|
99
|
+
client.get_passport("gate_my-api", "pp_xxx")
|
|
100
|
+
client.issue_passport("gate_my-api", agent_id="agent-001", permissions=["read"])
|
|
101
|
+
client.revoke_passport("gate_my-api", "pp_xxx")
|
|
102
|
+
client.reissue_passport("pp_xxx", accept_catalog_version=2)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Attestations
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
client.list_attestations("gate_my-api", limit=50)
|
|
109
|
+
client.record_attestation("gate_my-api", passport_id="pp_xxx", permission="read", tool_name="search", result="allowed")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Permission Catalog
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
client.get_catalog("gate_my-api")
|
|
116
|
+
client.create_catalog("gate_my-api", permissions=[...])
|
|
117
|
+
client.publish_catalog("gate_my-api", change_summary="Added search permission")
|
|
118
|
+
client.list_catalog_versions("gate_my-api")
|
|
119
|
+
client.get_catalog_version("gate_my-api", version=1)
|
|
120
|
+
client.get_catalog_impact("gate_my-api")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Gate Check
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
client.check_gate("gate_my-api", action="read", passport_id="pp_xxx")
|
|
127
|
+
client.authorize_dry_run(gate_id="gate_my-api", passport={"..."}, requested_permission="read")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Constraints
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
client.get_constraints("pp_xxx")
|
|
134
|
+
client.set_constraints("pp_xxx", {"read": {"core:rate:max_per_minute": 100}})
|
|
135
|
+
client.list_constraint_types(category="cost")
|
|
136
|
+
client.list_constraint_templates(category="security")
|
|
137
|
+
client.apply_constraint_template("pp_xxx", "conservative-agent")
|
|
138
|
+
client.create_constraint_template(slug="my-template", name="My Template", constraints={...})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Enforcement (CEL)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
client.enforce_action(passport_id="pp_xxx", action="write", cost_cents=500)
|
|
145
|
+
client.list_enforcement_attestations("pp_xxx", decision="BLOCK")
|
|
146
|
+
client.get_enforcement_attestation("enf_xxx")
|
|
147
|
+
client.verify_enforcement_attestation("enf_xxx")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Anonymous Access
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
client.get_anonymous_policy("gate_my-api")
|
|
154
|
+
client.set_anonymous_policy("gate_my-api", enabled=True, allowed_actions=["read"])
|
|
155
|
+
client.get_anonymous_log("gate_my-api")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Commerce
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
client.discover_services("flights:search", max_price_cents=100, sort="price_asc")
|
|
162
|
+
client.issue_consumption_attestation(passport_id="pp_xxx", gate_id="gate_my-api", action="flights:search", outcome="success")
|
|
163
|
+
client.generate_settlement(gate_id="gate_my-api", period_type="monthly", period_start="2025-01-01", period_end="2025-01-31")
|
|
164
|
+
client.list_settlements(gate_id="gate_my-api", status="pending")
|
|
165
|
+
client.get_settlement("stl_xxx")
|
|
166
|
+
client.update_settlement_status("stl_xxx", "invoiced")
|
|
167
|
+
client.get_sla_compliance("gate_my-api", period_start="2025-01-01", period_end="2025-01-31")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Cumulative State
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
client.get_cumulative_state("pp_xxx")
|
|
174
|
+
client.reset_cumulative_state("pp_xxx", window_type="daily")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### API Keys
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
client.list_api_keys()
|
|
181
|
+
client.create_api_key(name="My Key", scopes=["gates:read", "passports:write"])
|
|
182
|
+
client.revoke_api_key("key_xxx")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Cryptographic Verification
|
|
188
|
+
|
|
189
|
+
Local verification utilities for Ed25519 signatures and RFC 8785 content hashing:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from modus import verify_attestation_signature, verify_content_hash, compute_content_hash
|
|
193
|
+
|
|
194
|
+
valid = verify_attestation_signature(
|
|
195
|
+
attestation_json='{"gate_id":"gate_my-api",...}',
|
|
196
|
+
signature_b64="base64-sig...",
|
|
197
|
+
public_key_b64="base64-key...",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
matches = verify_content_hash(catalog_snapshot, expected_hash)
|
|
201
|
+
hash_hex = compute_content_hash({"key": "value"})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from modus import (
|
|
210
|
+
ModusError,
|
|
211
|
+
AuthenticationError,
|
|
212
|
+
AuthorizationError,
|
|
213
|
+
NotFoundError,
|
|
214
|
+
RateLimitError,
|
|
215
|
+
ValidationError,
|
|
216
|
+
ConflictError,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
client.get_gate("gate_nonexistent")
|
|
221
|
+
except NotFoundError as e:
|
|
222
|
+
print(f"Not found: {e}")
|
|
223
|
+
print(f"Status: {e.status_code}") # 404
|
|
224
|
+
except RateLimitError as e:
|
|
225
|
+
print(f"Rate limited, retry after: {e.retry_after}s")
|
|
226
|
+
except ModusError as e:
|
|
227
|
+
print(f"API error: {e} (HTTP {e.status_code})")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Exception | HTTP Status |
|
|
231
|
+
|-----------|-------------|
|
|
232
|
+
| `AuthenticationError` | 401 |
|
|
233
|
+
| `AuthorizationError` | 403 |
|
|
234
|
+
| `NotFoundError` | 404 |
|
|
235
|
+
| `ConflictError` | 409 |
|
|
236
|
+
| `ValidationError` | 400, 422 |
|
|
237
|
+
| `RateLimitError` | 429 |
|
|
238
|
+
| `ModusError` | All other errors |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Environment Variables
|
|
243
|
+
|
|
244
|
+
| Variable | Required | Default | Description |
|
|
245
|
+
|----------|----------|---------|-------------|
|
|
246
|
+
| `MODUS_API_KEY` | Yes | — | Your Modus API key |
|
|
247
|
+
| `MODUS_API_URL` | No | `https://modusoperator.com` | API base URL (override for local dev) |
|
|
248
|
+
|
|
249
|
+
You can also pass these directly to the client:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
client = ModusClient(api_key="mod_live_xxx", base_url="http://localhost:3000")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Documentation
|
|
258
|
+
|
|
259
|
+
Full documentation at [modusoperator.com/docs](https://modusoperator.com/docs)
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT — [Standard Logic Co.](https://standardlogic.ai)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "modus-sdk"
|
|
7
|
+
version = "1.1.2"
|
|
8
|
+
description = "Python SDK for the Modus REST API — manage agent passports, gates, and enforcement policies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Standard Logic Co.", email = "support@modusoperator.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"modus",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"permissions",
|
|
19
|
+
"passports",
|
|
20
|
+
"trust",
|
|
21
|
+
"mcp",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
dependencies = [
|
|
36
|
+
"httpx>=0.25.0",
|
|
37
|
+
"pydantic>=2.0.0",
|
|
38
|
+
"pynacl>=1.5.0",
|
|
39
|
+
"canonicaljson>=2.0.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0.0",
|
|
45
|
+
"pytest-asyncio>=0.21.0",
|
|
46
|
+
"respx>=0.21.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://modusoperator.com"
|
|
51
|
+
Documentation = "https://modusoperator.com/guide"
|
|
52
|
+
Repository = "https://github.com/StandardLogic/modus-mcp"
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
asyncio_mode = "auto"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/modus"]
|